diff --git a/.all-contributorsrc b/.all-contributorsrc
new file mode 100644
index 00000000..c5c3d814
--- /dev/null
+++ b/.all-contributorsrc
@@ -0,0 +1,224 @@
+{
+ "projectName": "react-universally",
+ "projectOwner": "ctrlplusb",
+ "files": [
+ "README.md"
+ ],
+ "imageSize": 100,
+ "commit": true,
+ "contributors": [
+ {
+ "login": "aoc",
+ "name": "Andrรฉs Calabrese",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/243161?v=3",
+ "profile": "https://github.com/aoc",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "andreyluiz",
+ "name": "Andrey Luiz",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/1965897?v=3",
+ "profile": "https://andreyluiz.github.io/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "alinporumb",
+ "name": "Alin Porumb",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/3148205?v=3",
+ "profile": "https://github.com/alinporumb",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "bkniffler",
+ "name": "Benjamin Kniffler",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/4349324?v=3",
+ "profile": "https://github.com/bkniffler",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "birkir",
+ "name": "Birkir Rafn Guรฐjรณnsson",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/180773?v=3",
+ "profile": "https://medium.com/@birkir.gudjonsson",
+ "contributions": [
+ "question",
+ "bug",
+ "code",
+ "review"
+ ]
+ },
+ {
+ "login": "carsonperrotti",
+ "name": "Carson Perrotti",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/2063102?v=3",
+ "profile": "http://carsonperrotti.com",
+ "contributions": [
+ "question",
+ "code",
+ "doc",
+ "review"
+ ]
+ },
+ {
+ "login": "LorbusChris",
+ "name": "Christian Glombek",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/13365531?v=3",
+ "profile": "https://github.com/LorbusChris",
+ "contributions": [
+ "bug",
+ "code"
+ ]
+ },
+ {
+ "login": "codepunkt",
+ "name": "Christoph Werner",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/603683?v=3",
+ "profile": "https://twitter.com/code_punkt",
+ "contributions": [
+ "question",
+ "bug",
+ "code",
+ "review"
+ ]
+ },
+ {
+ "login": "threehams",
+ "name": "David Edmondson",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/1399894?v=3",
+ "profile": "https://github.com/threehams",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "diondirza",
+ "name": "Dion Dirza",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/10954870?v=3",
+ "profile": "https://github.com/diondirza",
+ "contributions": [
+ "question",
+ "bug",
+ "code",
+ "doc",
+ "review"
+ ]
+ },
+ {
+ "login": "evgenyboxer",
+ "name": "Evgeny Boxer",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/254095?v=3",
+ "profile": "https://github.com/evgenyboxer",
+ "contributions": [
+ "bug",
+ "code"
+ ]
+ },
+ {
+ "login": "kohlmannj",
+ "name": "Joe Kohlmann",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/191304?v=3",
+ "profile": "http://kohlmannj.com",
+ "contributions": [
+ "bug",
+ "code"
+ ]
+ },
+ {
+ "login": "lucianlature",
+ "name": "Lucian Lature",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/24992?v=3",
+ "profile": "https://www.linkedin.com/in/lucianlature/",
+ "contributions": [
+ "bug",
+ "code",
+ "review"
+ ]
+ },
+ {
+ "login": "markshlick",
+ "name": "Mark Shlick",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/1624703?v=3",
+ "profile": "https://github.com/markshlick",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "rlindskog",
+ "name": "Ryan Lindskog",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/7436773?v=3",
+ "profile": "https://www.RyanLindskog.com/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "enten",
+ "name": "Steven Enten",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/977713?v=3",
+ "profile": "http://enten.fr",
+ "contributions": [
+ "question",
+ "bug",
+ "code",
+ "review"
+ ]
+ },
+ {
+ "login": "ctrlplusb",
+ "name": "Sean Matheson",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/12164768?v=3",
+ "profile": "http://www.ctrlplusb.com",
+ "contributions": [
+ "question",
+ "bug",
+ "code",
+ "doc",
+ "example",
+ "review",
+ "test",
+ "tool"
+ ]
+ },
+ {
+ "login": "strues",
+ "name": "Steven Truesdell",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/6218853?v=3",
+ "profile": "https://steventruesdell.com",
+ "contributions": [
+ "question",
+ "bug",
+ "code",
+ "doc",
+ "test"
+ ]
+ },
+ {
+ "login": "datoml",
+ "name": "Thomas Leitgeb",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/10552487?v=3",
+ "profile": "https://twitter.com/_datoml",
+ "contributions": [
+ "bug",
+ "code"
+ ]
+ },
+ {
+ "login": "tsnieman",
+ "name": "Tyler Nieman",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/595711?v=3",
+ "profile": "http://tsnieman.net/",
+ "contributions": [
+ "code"
+ ]
+ }
+ ]
+}
diff --git a/.env_example b/.env_example
index ca362573..eebde13a 100644
--- a/.env_example
+++ b/.env_example
@@ -9,11 +9,13 @@
# ==============================================================================
# The host on which to run the server.
-SERVER_HOST=localhost
+HOST=localhost
# The port on which to run the server.
-SERVER_PORT=1337
+PORT=1337
# The port on which to run the client bundle dev server (i.e. used during
# development only).
-CLIENT_DEVSERVER_PORT=7331
+CLIENT_DEV_PORT=7331
+
+GRAPHQL_URI=http://localhost:1337/graphql
diff --git a/.eslintignore b/.eslintignore
index 9e451b2d..b38db2f2 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,4 +1,2 @@
-flow-typed/
-tools/flow/
node_modules/
build/
diff --git a/.eslintrc b/.eslintrc
index 584dab27..ea5239bc 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,26 +1,28 @@
{
"parser": "babel-eslint",
"extends": "airbnb",
- "plugins": [
- "flowtype"
- ],
"env": {
"browser": true,
"es6": true,
"node": true,
"jest": true
},
- "ecmaFeatures": {
- "defaultParams": true
+ "parserOptions": {
+ "ecmaVersion": 6,
+ "sourceType": "module",
+ "ecmaFeatures": {
+ "defaultParams": true
+ }
},
"rules": {
- // We use the 'import' plugin which allows for cases "flow" awareness.
- "no-duplicate-imports": 0,
- // A .jsx extension is not required for files containing jsx.
+ // A jsx extension is not required for files containing jsx
"react/jsx-filename-extension": 0,
- // This rule struggles with flow and class properties.
+ // This rule struggles with flow and class properties
"react/sort-comp": 0,
- // We use global requires in various places, e.g. code splitting instances.
- "global-require": 0
+ // ignore linebreak style. the CRLF / LF endings wont matter
+ // if a windows user correctly converts CRLF to LF upon commits otherwise
+ // there are errors every line.
+ "linebreak-style": 0,
+ "jsx-a11y/href-no-hash": 0
}
}
diff --git a/.gitignore b/.gitignore
index 449da4d5..5aa7c063 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,11 +33,8 @@ npm-debug.log
.vscode
.history
-# flow-typed Lib Defs
-flow-typed/
-
-# Flow Coverage Report
-flow-coverage/
-
# Happypack
.happypack
+
+# OSX Files
+.DS_Store
diff --git a/.modernizrrc b/.modernizrrc
new file mode 100644
index 00000000..dd3400d8
--- /dev/null
+++ b/.modernizrrc
@@ -0,0 +1,7 @@
+{
+ "minify": true,
+ "options": [],
+ "feature-detects": [
+ "elem/picture"
+ ]
+}
diff --git a/.nvmrc b/.nvmrc
index 7742c2f0..737665a5 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-v6.9.2
+v6.11.1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e40dc6c..60c2a78a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,47 @@ I'll map them as follows:
- Minor: New features or changes to the build tools. Could contain some things that are traditionally know as breaking changes, however, I believe the upgrade path to minor.
- Patch: Small(ish) fixes/restructuring that I expect will take minimal effort to merge in.
+# [13.0.0] - 2017-04-05
+
+### BREAKING
+
+ - Renames the 'development' command to 'develop'.
+ - Big folder structure refactor, moving items from src/* into the root of the project.
+ - Renames the CONF_ENV variable to DEPLOYMENT for targetting of .env.{environment} environment files.
+ - Upgrades to `react-router` v4.
+ - Replaces `code-split-component` with `react-async-component`
+ - Complete restructure of the DefinePlugin special flags, they have been prefixed with "BUILD_FLAG_" to make them more obvious when used in the code. This also helps us distinguish these build-time values from other runtime provided process.env values.
+ - Removes cross-env and refactors the script commands. You can assign NODE_ENV as and when you need now (for example, to target a .env.production environment configuration file).
+ - Renamed environment variables:
+ - `SERVER_PORT` to `PORT`
+ - `SERVER_HOST` to `HOST`
+ - `CLIENT_DEVSERVER_PORT` to `CLIENT_DEV_PORT`
+ - Renames the `nodeBundlesIncludeNodeModuleFileTypes` config property to `nodeExternalsFileTypeWhitelist`
+ - Refactors the server and serviceworker offline page generation. We now use a set of React components (`ServerHTML` and `HTML`) to manage our HTML in a uniform fashion.
+ - Refactors the client configuration filter rule to be contained within the main configuration and moves the configuration object creation into the server middleware.
+ - Refactors the config folder in various ways. Cleaning up, restructuring, etc.
+ - Renames the `environmentVars` file and helpers.
+ - Moves all the HTML head tags into the DemoApp helmet configuration.
+
+### Changed
+
+ - All server/client/shared code all use the shared config helper.
+ - Updated dependencies, including to the latest Webpack official 2 release.
+
+### Added
+
+ - New babel plugins to optimise React production build performance.
+ - Adds new icon sets.
+ - Prettier
+ - Some basic global styling via milligram
+
+### Fixed
+
+ - Chrome favicon request issue.
+ - Cleans up the package scripts.
+ - Service worker would fail if a subfolder was added to the public folder.
+ - Tons of other things. :)
+
# [12.0.0] - 2017-01-09
### BREAKING
diff --git a/LICENSE b/LICENSE
index 6221cf1b..8a7d8283 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2016 Sean Matheson
+Copyright (c) 2017 Sean Matheson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 3ca8b1ae..9d35e0a5 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,38 @@
+### Feature Branch
+
+Note: This is a feature branch of `react-universally`. Please see the [`FEATURE_REDUX_OPINIONATED.md`](/docs/FEATURE_REDUX_OPINIONATED.md) for more information on this branch.
+
+---
+
React, Universally
-
A starter kit giving you the minimum requirements for a modern universal React application.
+
A starter kit for universal react applications.
+[![All Contributors](https://img.shields.io/badge/all_contributors-20-orange.svg?style=flat-square)](#contributors)
+
## About
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).
+> 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.
+
## Features
- ๐ `react` as the view.
- ๐ `react-router` v4 as the router.
- ๐ `express` server.
- ๐ญ `jest` as the test framework.
+ - ๐ Combines `prettier` and Airbnb's ESlint configuration - performing code formatting on commit. Stop worrying about code style consistency.
- ๐ Very basic CSS support - it's up to you to extend it with CSS Modules etc.
- - โ๏ธ Code splitting - easily define code split points in your source using `code-split-component`.
+ - โ๏ธ Code splitting - easily define code split points in your source using `react-async-component`.
- ๐ Server Side Rendering.
- ๐ Progressive Web Application ready, with offline support, via a Service Worker.
- ๐ Long term browser caching of assets with automated cache invalidation.
- ๐ฆ All source is bundled using Webpack v2.
- - ๐ Full ES2017+ support - use the exact same JS syntax across the entire project (src/tools/config). No more folder context switching! We also only use syntax that is stage-3 or later in the TC39 process.
- - ๐ง Centralised application configuration with helpers to avoid boilerplate in your code.
+ - ๐ 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.
+ - ๐ง Centralised application configuration with helpers to avoid boilerplate in your code. Also has support for environment specific configuration files.
- ๐ฅ 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.
- โ SEO friendly - `react-helmet` provides control of the page title/meta/styles/scripts from within your components.
- ๐ค Optimised Webpack builds via HappyPack and an auto generated Vendor DLL for smooth development experiences.
@@ -29,12 +40,11 @@ This starter kit contains all the build tooling and configuration you need to ki
- ๐ฎ Security on the `express` server using `helmet` and `hpp`.
- ๐ Asset bundling support. e.g. images/fonts.
- ๐ Preconfigured to support development and optimised production builds.
- - ๐ผ Airbnb's ESlint configuration.
- โค๏ธ Preconfigured to deploy to `now` with a single command.
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.
-> 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`](/docs/FEATURE_BRANCHES.md) documentation for more.
+> 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.
## Getting started
@@ -42,7 +52,7 @@ Redux/MobX, data persistence, modern styling frameworks and all the other bells
git clone https://github.com/ctrlplusb/react-universally my-project
cd my-project
yarn
-yarn run development
+yarn run develop
```
Or, if you aren't using [`yarn`](https://yarnpkg.com/):
@@ -51,17 +61,30 @@ Or, if you aren't using [`yarn`](https://yarnpkg.com/):
git clone https://github.com/ctrlplusb/react-universally my-project
cd my-project
npm install
-npm run development
+npm run develop
```
Now go make some changes to the `Home` component to see the tooling in action.
## Docs
- - [Project Overview](/docs/PROJECT_OVERVIEW.md)
- - [Application Configuration](/docs/APPLICATION_CONFIG.md)
- - [Package Script Commands](/docs/PKG_SCRIPTS.md)
- - [Feature Branches](/docs/FEATURE_BRANCHES.md)
- - [Deploy your very own Server Side Rendering React App in 5 easy steps](/docs/DEPLOY_TO_NOW.md)
- - [FAQ](/docs/FAQ.md)
+ - [Project Overview](/internal/docs/PROJECT_OVERVIEW.md)
+ - [Project Configuration](/internal/docs/PROJECT_CONFIG.md)
+ - [Package Script Commands](/internal/docs/PKG_SCRIPTS.md)
+ - [FAQ](/internal/docs/FAQ.md)
+ - [Feature Branches](/internal/docs/FEATURE_BRANCHES.md)
+ - [Deploy your very own Server Side Rendering React App in 5 easy steps](/internal/docs/DEPLOY_TO_NOW.md)
- [Changelog](/CHANGELOG.md)
+
+## Contributors
+
+Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):
+
+
+| [ 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) |
+| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
+| [ 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) |
+| [ 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) |
+
+
+This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
diff --git a/src/client/components/ReactHotLoader.js b/client/components/ReactHotLoader.js
similarity index 70%
rename from src/client/components/ReactHotLoader.js
rename to client/components/ReactHotLoader.js
index 08a65fdc..18e53476 100644
--- a/src/client/components/ReactHotLoader.js
+++ b/client/components/ReactHotLoader.js
@@ -3,10 +3,9 @@
import React from 'react';
-// We create this wrapper so that we only import react-hot-laoder for a
+// We create this wrapper so that we only import react-hot-loader for a
// development build. Small savings. :)
-const ReactHotLoader =
- process.env.NODE_ENV === 'development'
+const ReactHotLoader = process.env.NODE_ENV === 'development'
? require('react-hot-loader').AppContainer
: ({ children }) => React.Children.only(children);
diff --git a/client/index.js b/client/index.js
new file mode 100644
index 00000000..4138a9dd
--- /dev/null
+++ b/client/index.js
@@ -0,0 +1,98 @@
+/* eslint-disable global-require, no-underscore-dangle */
+// we need a fetch polyfill for both the browser and server for Apollo.
+import "isomorphic-fetch/fetch-npm-browserify";
+import React from "react";
+import { render } from "react-dom";
+import BrowserRouter from "react-router-dom/BrowserRouter";
+import asyncBootstrapper from "react-async-bootstrapper";
+import { AsyncComponentProvider } from "react-async-component";
+import { ApolloProvider } from "react-apollo";
+import configureStore from "../shared/redux/configureStore";
+import { createApolloClient, getNetworkInterface } from "../shared/apollo";
+
+import "./polyfills";
+
+import ReactHotLoader from "./components/ReactHotLoader";
+import DemoApp from "../shared/components/DemoApp";
+
+// Get the DOM Element that will host our React application.
+const container = document.querySelector("#app");
+
+const preloadedState = window.__APOLLO_STATE__;
+
+// Apollo setup
+// all options described below
+// @see http://dev.apollodata.com/core/apollo-client-api.html#constructor
+const clientOptions = {
+ initialState: preloadedState,
+ ssrForceFetchDelay: 100,
+ connectToDevTools: true
+};
+const apolloClient = createApolloClient({
+ clientOptions,
+ networkInterface: getNetworkInterface()
+});
+
+// Create our Redux store.
+const store = configureStore(
+ apolloClient,
+ // Server side rendering would have mounted our state on this global.
+ preloadedState
+);
+
+// Does the user's browser support the HTML5 history API?
+// If the user's browser doesn't support the HTML5 history API then we
+// will force full page refreshes on each page change.
+const supportsHistory = "pushState" in window.history;
+
+// Get any rehydrateState for the async components.
+// eslint-disable-next-line no-underscore-dangle
+const asyncComponentsRehydrateState =
+ window.__ASYNC_COMPONENTS_REHYDRATE_STATE__;
+
+/**
+ * Renders the given React Application component.
+ */
+function renderApp(TheApp) {
+ // Firstly, define our full application component, wrapping the given
+ // component app with a browser based version of react router.
+ const app = (
+
+
+
+
+
+
+
+
+
+ );
+
+ // We use the react-async-component in order to support code splitting of
+ // our bundle output. It's important to use this helper.
+ // @see https://github.com/ctrlplusb/react-async-component
+ asyncBootstrapper(app).then(() => render(app, container));
+}
+
+// Execute the first render of our app.
+renderApp(DemoApp);
+
+// This registers our service worker for asset caching and offline support.
+// Keep this as the last item, just in case the code execution failed (thanks
+// to react-boilerplate for that tip.)
+require("./registerServiceWorker");
+
+// The following is needed so that we can support hot reloading our application.
+if (process.env.BUILD_FLAG_IS_DEV === "true" && module.hot) {
+ module.hot.dispose(data => {
+ // Deserialize store and keep in hot module data for next replacement
+ data.store = stringify(toJS(store)); // eslint-disable-line
+ });
+
+ // Accept changes to this file for hot reloading.
+ module.hot.accept("./index.js");
+ // Any changes to our App will cause a hotload re-render.
+ module.hot.accept("../shared/components/DemoApp", () => {
+ renderApp(require("../shared/components/DemoApp").default);
+ });
+}
diff --git a/client/polyfills/index.js b/client/polyfills/index.js
new file mode 100644
index 00000000..af2bbca3
--- /dev/null
+++ b/client/polyfills/index.js
@@ -0,0 +1,18 @@
+/* eslint-disable no-console */
+
+import Modernizr from 'modernizr';
+
+// This is just an illustrative example. Here you are testing the client's
+// support for the "picture" element, and if it isn't supported then you
+// load a polyfill.
+if (!Modernizr.picture) {
+ console.log('Client does not support "picture", polyfilling it...');
+ // If you want to use the below do a `yarn add picturefill --exact` and then
+ // uncomment the lines below:
+ /*
+ require('picturefill');
+ require('picturefill/dist/plugins/mutation/pf.mutation');
+ */
+} else {
+ console.log('Client has support for "picture".');
+}
diff --git a/src/client/registerServiceWorker.js b/client/registerServiceWorker.js
similarity index 65%
rename from src/client/registerServiceWorker.js
rename to client/registerServiceWorker.js
index 70032271..af402f6a 100644
--- a/src/client/registerServiceWorker.js
+++ b/client/registerServiceWorker.js
@@ -1,17 +1,20 @@
-// We use the offline-plugin to generate a service worker. See the webpack
-// config for more details.
-//
-// We need to ensure that the runtime is installed so that the generated
-// service worker is executed.
-//
-// We will only be doing this for production builds.
+/**
+ * We use the offline-plugin to generate a service worker. See the webpack
+ * config for more details.
+ *
+ * We need to ensure that the runtime is installed so that the generated
+ * service worker is executed.
+ *
+ * NOTE: We only enable the service worker for non-development environments.
+ */
-import { safeConfigGet } from '../shared/utils/config';
+import config from '../config';
-if (process.env.NODE_ENV === 'production') {
+if (process.env.BUILD_FLAG_IS_DEV === 'false') {
// We check the shared config, ensuring that the service worker has been
// enabled.
- if (safeConfigGet(['serviceWorker', 'enabled'])) {
+ if (config('serviceWorker.enabled')) {
+ // eslint-disable-next-line global-require
const OfflinePluginRuntime = require('offline-plugin/runtime');
// Install the offline plugin, which instantiates our service worker and app
diff --git a/config/components/ClientConfig.js b/config/components/ClientConfig.js
new file mode 100644
index 00000000..dacb92e9
--- /dev/null
+++ b/config/components/ClientConfig.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import serialize from 'serialize-javascript';
+import filterWithRules from '../../shared/utils/objects/filterWithRules';
+import values from '../values';
+
+// Filter the config down to the properties that are allowed to be included
+// in the HTML response.
+const clientConfig = filterWithRules(
+ // These are the rules used to filter the config.
+ values.clientConfigFilter,
+ // The config values to filter.
+ values,
+);
+
+const serializedClientConfig = serialize(clientConfig);
+
+/**
+ * A react component that generates a script tag that binds the allowed
+ * values to the window so that config values can be read within the
+ * browser.
+ *
+ * They get bound to window.__CLIENT_CONFIG__
+ */
+function ClientConfig({ nonce }) {
+ return (
+
+ );
+}
+
+ClientConfig.propTypes = {
+ nonce: PropTypes.string.isRequired,
+};
+
+export default ClientConfig;
diff --git a/config/index.js b/config/index.js
index 0f5baca2..660c7381 100644
--- a/config/index.js
+++ b/config/index.js
@@ -1,395 +1,115 @@
-// Application Configuration.
-//
-// Please see the /docs/APPLICATION_CONFIG.md documentation for more info.
-//
-// Note: all file/folder paths should be relative to the project root. The
-// absolute paths should be resolved during runtime by our build tools/server.
-
-import { getStringEnvVar, getIntEnvVar } from './internals/environmentVars';
-import filterObject from './internals/filterObject';
-
-
-// This protects us from accidentally including this configuration in our
-// client bundle. That would be a big NO NO to do. :)
-if (process.env.IS_CLIENT) {
- throw new Error("You shouldn't be importing the `./config` directly into your 'client' or 'shared' source as the configuration object will get included in your client bundle. Not a safe move! Instead, use the `safeConfigGet` helper function (located at `./src/shared/utils/config`) within the 'client' or 'shared' source files to reference configuration values in a safe manner.");
+/**
+ * Unified Configuration Reader
+ *
+ * This helper function allows you to use the same API in accessing configuration
+ * values no matter where the code is being executed (i.e. browser/node).
+ *
+ * e.g.
+ * import config from '../config';
+ * config('welcomeMessage'); // => "Hello World!"
+ */
+
+/* eslint-disable no-console */
+/* eslint-disable import/global-require */
+/* eslint-disable no-underscore-dangle */
+
+// PRIVATES
+
+let configCache;
+
+/**
+ * This resolves the correct configuration source based on the execution
+ * environment. For node we use the standard config file, however, for browsers
+ * we need to access the configuration object that would have been bound to
+ * the "window" by our "reactApplication" middleware.
+ *
+ * @return {Object} The executing environment configuration object.
+ */
+function resolveConfigForBrowserOrServer() {
+ if (configCache) {
+ return configCache;
+ }
+
+ // NOTE: By using the "process.env.BUILD_FLAG_IS_NODE" flag here this block of code
+ // will be removed when "process.env.BUILD_FLAG_IS_NODE === true".
+ // If no "BUILD_FLAG_IS_NODE" env var is undefined we can assume that we are running outside
+ // of a webpack run, and will therefore return the config file.
+ if (
+ typeof process.env.BUILD_FLAG_IS_NODE === 'undefined' ||
+ process.env.BUILD_FLAG_IS_NODE === 'true'
+ ) {
+ // i.e. running in our server/node process.
+ // eslint-disable-next-line global-require
+ configCache = require('./values').default;
+ return configCache;
+ }
+
+ // To get here we are likely running in the browser.
+
+ if (typeof window !== 'undefined' && typeof window.__CLIENT_CONFIG__ === 'object') {
+ configCache = window.__CLIENT_CONFIG__;
+ } else {
+ // To get here we must be running in the browser.
+ console.warn('No client configuration object was bound to the window.');
+ configCache = {};
+ }
+
+ return configCache;
}
-const config = {
- // The host on which the server should run.
- host: getStringEnvVar('SERVER_HOST', 'localhost'),
-
- // The port on which the server should run.
- port: getIntEnvVar('SERVER_PORT', 1337),
-
- // The port on which the client bundle development server should run.
- clientDevServerPort: getIntEnvVar('CLIENT_DEVSERVER_PORT', 7331),
-
- // This is an example environment variable which is consumed within the
- // './client.js' config. See there for more details.
- welcomeMessage: getStringEnvVar('WELCOME_MSG', 'Hello world!'),
-
- // Disable server side rendering?
- disableSSR: false,
-
- // How long should we set the browser cache for the served assets?
- // Don't worry, we add hashes to the files, so if they change the new files
- // will be served to browsers.
- // We are using the "ms" format to set the length.
- // @see https://www.npmjs.com/package/ms
- browserCacheMaxAge: '365d',
-
- // Path to the public assets that will be served off the root of the
- // HTTP server.
- publicAssetsPath: './public',
-
- // Where does our build output live?
- buildOutputPath: './build',
-
- // Should we optimize production builds (i.e. minify etc).
- // Sometimes you don't want this to happen to aid in debugging complex
- // problems. Having this configuration flag here allows you to quickly
- // toggle the feature.
- optimizeProductionBuilds: true,
-
- // Do you want to included source maps (will be served as seperate files)
- // for production builds?
- includeSourceMapsForProductionBuilds: false,
-
- // Path to the shared src between the bundles.
- bundlesSharedSrcPath: './src/shared',
-
- // These extensions are tried when resolving src files for our bundles..
- bundleSrcTypes: ['js', 'jsx', 'json'],
-
- // Additional asset types to be supported for our bundles.
- // i.e. you can import the following file types within your source and the
- // webpack bundling process will bundle them with your source and create
- // URLs for them that can be resolved at runtime.
- bundleAssetTypes: [
- 'jpg',
- 'jpeg',
- 'png',
- 'gif',
- 'ico',
- 'eot',
- 'svg',
- 'ttf',
- 'woff',
- 'woff2',
- 'otf',
- ],
-
- // What should we name the json output file that webpack generates
- // containing details of all output files for a bundle?
- bundleAssetsFileName: 'assets.json',
-
- // Extended configuration for the Content Security Policy (CSP)
- // @see src/server/middleware/security for more info.
- cspExtensions: {
- childSrc: [],
- connectSrc: [],
- defaultSrc: [],
- fontSrc: [],
- imgSrc: [],
- mediaSrc: [],
- manifestSrc: [],
- objectSrc: [],
- scriptSrc: [],
- styleSrc: [],
- },
-
- // node_modules are not included in any bundles that target "node" as a runtime
- // (i.e. the server bundle).
- // The node_modules may however contain files that will need to be processed by
- // one of our webpack loaders.
- // Add any required file types to the list below.
- nodeBundlesIncludeNodeModuleFileTypes: [
- /\.(eot|woff|woff2|ttf|otf)$/,
- /\.(svg|png|jpg|jpeg|gif|ico)$/,
- /\.(mp4|mp3|ogg|swf|webp)$/,
- /\.(css|scss|sass|sss|less)$/,
- ],
-
- // Note: you can only have a single service worker instance. Our service
- // worker implementation is bound to the "client" and "server" bundles.
- // It includes the "client" bundle assets, as well as the public folder assets,
- // and it is served by the "server" bundle.
- serviceWorker: {
- // Enabled?
- enabled: true,
- // Service worker name
- fileName: 'sw.js',
- // Paths to the public assets which should be included within our
- // service worker. Relative to our public folder path, and accepts glob
- // syntax.
- includePublicAssets: [
- // NOTE: This will include ALL of our public folder assets. We do
- // a glob pull of them and then map them to /foo paths as all the
- // public folder assets get served off the root of our application.
- // You may or may not want to be including these assets. Feel free
- // to remove this or instead include only a very specific set of
- // assets.
- './**/*',
- ],
- // Path to the template used by HtmlWebpackPlugin to generate an offline
- // page that will be used by the service worker to render our application
- // offline.
- offlinePageTemplate: './tools/webpack/offlinePage',
- // Offline page file name.
- offlinePageFileName: 'offline.html',
- },
-
- // We use the polyfill.io service which provides the polyfills that a
- // client needs, which is far more optimal than the large output
- // generated by babel-polyfill.
- // Note: we have to keep this seperate from our "htmlPage" configuration
- // as the polyfill needs to be loaded BEFORE any of our other javascript
- // gets parsed.
- polyfillIO: {
- enabled: true,
- url: 'https://cdn.polyfill.io/v2/polyfill.min.js',
- },
-
- // Configuration for the HTML pages (headers/titles/scripts/css/etc).
- // We make use of react-helmet to consume the values below.
- // @see https://github.com/nfl/react-helmet
- htmlPage: {
- htmlAttributes: { lang: 'en' },
- titleTemplate: 'React, Universally - %s',
- defaultTitle: 'React, Universally',
- meta: [
- {
- name: 'description',
- content: 'A starter kit giving you the minimum requirements for a production ready universal react application.',
- },
- // Default content encoding.
- { name: 'charset', content: 'utf-8' },
- // @see http://bit.ly/2f8IaqJ
- { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' },
- // This is important to signify your application is mobile responsive!
- { name: 'viewport', content: 'width=device-width, initial-scale=1' },
- // Providing a theme color is good if you are doing a progressive
- // web application.
- { name: 'theme-color', content: '#2b2b2b' },
- ],
- links: [
- // When building a progressive web application you need to supply
- // a manifest.json as well as a variety of icon types. This can be
- // tricky. Luckily there is a service to help you with this.
- // http://realfavicongenerator.net/
- { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' },
- { rel: 'icon', type: 'image/png', href: '/favicon-32x32.png', sizes: '32x32' },
- { rel: 'icon', type: 'image/png', href: '/favicon-16x16.png', sizes: '16x16' },
- { rel: 'mask-icon', href: '/safari-pinned-tab.svg', color: '#00a9d9' },
- // Make sure you update your manifest.json to match your application.
- { rel: 'manifest', href: '/manifest.json' },
- ],
- scripts: [
- // Example:
- // { src: 'http://include.com/pathtojs.js', type: 'text/javascript' },
- ],
- },
-
- bundles: {
- client: {
- // Src entry file.
- srcEntryFile: './src/client/index.js',
-
- // Src paths.
- srcPaths: [
- './src/client',
- './src/shared',
- // The service worker offline page generation needs access to the
- // config folder. Don't worry we have guards within the config files
- // to ensure they never get included in a client bundle.
- './config',
- ],
-
- // Where does the client bundle output live?
- outputPath: './build/client',
-
- // What is the public http path at which we must serve the bundle from?
- webPath: '/client/',
-
- // Configuration settings for the development vendor DLL. This will be created
- // by our development server and provides an improved dev experience
- // by decreasing the number of modules that webpack needs to process
- // for every rebuild of our client bundle. It by default uses the
- // dependencies configured in package.json however you can customise
- // which of these dependencies are excluded, whilst also being able to
- // specify the inclusion of additional modules below.
- devVendorDLL: {
- // Enabled?
- enabled: true,
-
- // Specify any dependencies that you would like to include in the
- // Vendor DLL.
- //
- // NOTE: It is also possible that some modules require specific
- // webpack loaders in order to be processed (e.g. CSS/SASS etc).
- // For these cases you don't want to include them in the Vendor DLL.
- include: [
- 'code-split-component',
- 'react',
- 'react-dom',
- 'react-helmet',
- 'react-router',
- ],
-
- // The name of the vendor DLL.
- name: '__dev_vendor_dll__',
- },
- },
-
- server: {
- // Src entry file.
- srcEntryFile: './src/server/index.js',
-
- // Src paths.
- srcPaths: [
- './src/server',
- './src/shared',
- './config',
- ],
-
- // Where does the server bundle output live?
- outputPath: './build/server',
- },
- },
-
- additionalNodeBundles: {
- // NOTE: The webpack configuration and build scripts have been built so
- // that you can add arbitrary additional node bundle configurations here.
- //
- // A common requirement for larger projects is to add additional "node"
- // target bundles (e.g an APi server endpoint). Therefore flexibility has been
- // baked into our webpack config factory to allow for this.
- //
- // Simply define additional configurations similar to below. The development
- // server will manage starting them up for you. The only requirement is that
- // within the entry for each bundle you create and return the "express"
- // listener.
- /*
- apiServer: {
- srcEntryFile: './src/api/index.js',
- srcPaths: [
- './src/api',
- './src/shared',
- './config',
- ],
- outputPath: './build/api',
- }
- */
- },
-
- // These plugin definitions provide you with advanced hooks into customising
- // the project without having to reach into the internals of the tools.
- //
- // We have decided to create this plugin approach so that you can come to
- // a centralised configuration folder to do most of your application
- // configuration adjustments. Additionally it helps to make merging
- // from the origin starter kit a bit easier.
- plugins: {
- // This plugin allows you to provide final adjustments your babel
- // configurations for each bundle before they get processed.
- //
- // This function will be called once for each for your bundles. It will be
- // provided the current webpack config, as well as the buildOptions which
- // detail which bundle and mode is being targetted for the current function run.
- babelConfig: (babelConfig, buildOptions) => {
- // eslint-disable-next-line no-unused-vars
- const { target, mode } = buildOptions;
-
- // Example
- /*
- if (target === 'server' && mode === 'development') {
- babelConfig.presets.push('foo');
+// EXPORT
+
+/**
+ * This function wraps up the boilerplate needed to access the correct
+ * configuration depending on whether your code will get executed in the
+ * browser/node.
+ *
+ * i.e.
+ * - For the browser the config values are available at window.__CLIENT_CONFIG__
+ * - For a node process they are within the "/config".
+ *
+ * To request a configuration value you must provide the repective path. For
+ * example, f you had the following configuration structure:
+ * {
+ * foo: {
+ * bar: [1, 2, 3]
+ * },
+ * bob: 'bob'
+ * }
+ *
+ * You could use this function to access "bar" like so:
+ * import config from '../config';
+ * const value = config('foo.bar');
+ *
+ * And you could access "bob" like so:
+ * import config from '../config';
+ * const value = config('bob');
+ *
+ * If any part of the path isn't available as a configuration key/value then
+ * an error will be thrown indicating that a respective configuration value
+ * could not be found at the given path.
+ */
+export default function configGet(path) {
+ const parts = typeof path === 'string' ? path.split('.') : path;
+
+ if (parts.length === 0) {
+ throw new Error(
+ 'You must provide the path to the configuration value you would like to consume.',
+ );
+ }
+ let result = resolveConfigForBrowserOrServer();
+ for (let i = 0; i < parts.length; i += 1) {
+ if (result === undefined) {
+ const errorMessage = `Failed to resolve configuration value at "${parts.join('.')}".`;
+ // This "if" block gets stripped away by webpack for production builds.
+ if (process.env.BUILD_FLAG_IS_DEV === 'true' && process.env.BUILD_FLAG_IS_CLIENT === 'true') {
+ throw new Error(
+ `${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.`,
+ );
}
- */
-
- return babelConfig;
- },
-
- // This plugin allows you to provide final adjustments your webpack
- // configurations for each bundle before they get processed.
- //
- // I would recommend looking at the "webpack-merge" module to help you with
- // merging modifications to each config.
- //
- // This function will be called once for each for your bundles. It will be
- // provided the current webpack config, as well as the buildOptions which
- // detail which bundle and mode is being targetted for the current function run.
- webpackConfig: (webpackConfig, buildOptions) => {
- // eslint-disable-next-line no-unused-vars
- const { target, mode } = buildOptions;
-
- // Example:
- /*
- if (target === 'server' && mode === 'development') {
- webpackConfig.plugins.push(new MyCoolWebpackPlugin());
- }
- */
-
- // Debugging/Logging Example:
- /*
- if (target === 'server') {
- console.log(JSON.stringify(webpackConfig, null, 4));
- }
- */
-
- return webpackConfig;
- },
- },
-};
-
-// Export the client configuration object.
-export const clientConfig = filterObject(
- // We will filter our full application configuration object...
- config,
- // using the rules below in order to create our filtered client configuration
- // object.
- //
- // This object will be bound to the window.__CLIENT_CONFIG__
- // property which is where client code should be referencing it from.
- // As we generally have shared code between our node/browser code we have
- // created a helper function in "./src/shared/utils/config" that you can used
- // to request config values from. It will make sure that either the
- // application config file is used (i.e. this file), or the
- // window.__CLIENT_CONFIG__ is used. This avoids boilerplate throughout your
- // shared code. We recommend using this helper anytime you need a config
- // value within either the "client" or "shared" folder (i.e. any folders
- // that contain code which will end up in the browser).
- //
- // This is a filter that will be applied to our configuration in order to
- // determine which of our configuration values will be provided to the client
- // bundle.
- //
- // For security reasons you wouldn't want to make all of the configuration values
- // accessible by client bundles as these values would essentially be getting
- // transported over the wire to user's browsers. There are however cases
- // where you may want to expose one or two of the values within a client bundle.
- //
- // This filter object must match the shape of the configuration object, however
- // you need not specify every property that is defined within the configuration
- // object. Simply define the properties you would like to be included in the
- // client config, supplying a truthy value to them in order to ensure they
- // get included in the client bundle.
- {
- // This is here as an example showing that you can expose environment
- // variables too.
- welcomeMessage: true,
- // We only need to expose the enabled flag of the service worker.
- serviceWorker: {
- enabled: true,
- },
- // We need to expose all the polyfill.io settings.
- polyfillIO: true,
- // We need to expose all the htmlPage settings.
- htmlPage: true,
- additionalNodeBundles: true,
- },
-);
-
-// Export the main config as the default export.
-export default config;
+ throw new Error(errorMessage);
+ }
+ result = result[parts[i]];
+ }
+ return result;
+}
diff --git a/config/internals/environmentVars.js b/config/internals/environmentVars.js
deleted file mode 100644
index cf64a5d7..00000000
--- a/config/internals/environmentVars.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import dotenv from 'dotenv';
-import fs from 'fs';
-import path from 'path';
-import appRootDir from 'app-root-dir';
-import userHome from 'user-home';
-import colors from 'colors/safe';
-import pkg from '../../package.json';
-
-function registerEnvFile() {
- const envName = process.env.NODE_ENV || 'development';
- const envFile = '.env';
-
- // This is the order in which we will try to resolve an environment configuration
- // file.
- const envFileResolutionOrder = [
- // Is there an environment config file at the app root for our target
- // environment name?
- // e.g. /projects/react-universally/.env.development
- path.resolve(appRootDir.get(), `${envFile}.${envName}`),
- // Is there an environment config file at the app root?
- // e.g. /projects/react-universally/.env
- path.resolve(appRootDir.get(), envFile),
- // Is there an environment config file in the executing user's home dir
- // that is targetting the specific environment?
- // e.g. /Users/ctrlplusb/.config/react-universally/.env.development
- path.resolve(userHome, '.config', pkg.name, `${envFile}.${envName}`),
- // Is there an environment config file in the executing user's home dir?
- // e.g. /Users/ctrlplusb/.config/react-universally/.env
- path.resolve(userHome, '.config', pkg.name, envFile),
- ];
-
- // Find the first env file path match.
- const envFilePath = envFileResolutionOrder.find(filePath => fs.existsSync(filePath));
-
- // If we found an env file match the register it.
- if (envFilePath) {
- console.log( // eslint-disable-line no-console
- colors.bgBlue.white(`==> Registering environment variables from: ${envFilePath}`),
- );
- dotenv.config({ path: envFilePath });
- }
-}
-
-// Ensure that we first register any environment variables from an existing
-// env file.
-registerEnvFile();
-
-export function getStringEnvVar(name, defaultVal) {
- return process.env[name] || defaultVal;
-}
-
-export function getIntEnvVar(name, defaultVal) {
- return process.env[name]
- ? parseInt(process.env[name], 10)
- : defaultVal;
-}
-
-export function getBoolVar(name, defaultVal) {
- return process.env[name]
- ? process.env[name] === 'true'
- : defaultVal;
-}
diff --git a/config/utils/envVars.js b/config/utils/envVars.js
new file mode 100644
index 00000000..12c069ed
--- /dev/null
+++ b/config/utils/envVars.js
@@ -0,0 +1,80 @@
+/**
+ * Helper for resolving environment specific configuration files.
+ *
+ * It resolves .env files that are supported by the `dotenv` library.
+ *
+ * Please read the application configuration docs for more info.
+ */
+
+import appRootDir from 'app-root-dir';
+import colors from 'colors/safe';
+import dotenv from 'dotenv';
+import fs from 'fs';
+import path from 'path';
+
+import ifElse from '../../shared/utils/logic/ifElse';
+import removeNil from '../../shared/utils/arrays/removeNil';
+
+// PRIVATES
+
+function registerEnvFile() {
+ const DEPLOYMENT = process.env.DEPLOYMENT;
+ const envFile = '.env';
+
+ // This is the order in which we will try to resolve an environment configuration
+ // file.
+ const envFileResolutionOrder = removeNil([
+ // Is there an environment config file at the app root?
+ // This always takes preference.
+ // e.g. /projects/react-universally/.env
+ path.resolve(appRootDir.get(), envFile),
+ // Is there an environment config file at the app root for our target
+ // environment name?
+ // e.g. /projects/react-universally/.env.staging
+ ifElse(DEPLOYMENT)(path.resolve(appRootDir.get(), `${envFile}.${DEPLOYMENT}`)),
+ ]);
+
+ // Find the first env file path match.
+ const envFilePath = envFileResolutionOrder.find(filePath => fs.existsSync(filePath));
+
+ // If we found an env file match the register it.
+ if (envFilePath) {
+ // eslint-disable-next-line no-console
+ console.log(colors.bgBlue.white(`==> Registering environment variables from: ${envFilePath}`));
+ dotenv.config({ path: envFilePath });
+ }
+}
+
+// Ensure that we first register any environment variables from an existing
+// env file.
+registerEnvFile();
+
+// EXPORTED HELPERS
+
+/**
+ * Gets a string environment variable by the given name.
+ *
+ * @param {String} name - The name of the environment variable.
+ * @param {String} defaultVal - The default value to use.
+ *
+ * @return {String} The value.
+ */
+export function string(name, defaultVal) {
+ return process.env[name] || defaultVal;
+}
+
+/**
+ * Gets a number environment variable by the given name.
+ *
+ * @param {String} name - The name of the environment variable.
+ * @param {number} defaultVal - The default value to use.
+ *
+ * @return {number} The value.
+ */
+export function number(name, defaultVal) {
+ return process.env[name] ? parseInt(process.env[name], 10) : defaultVal;
+}
+
+export function bool(name, defaultVal) {
+ return process.env[name] ? process.env[name] === 'true' || process.env[name] === '1' : defaultVal;
+}
diff --git a/config/values.js b/config/values.js
new file mode 100644
index 00000000..e81dd1e8
--- /dev/null
+++ b/config/values.js
@@ -0,0 +1,328 @@
+/**
+ * Project Configuration.
+ *
+ * NOTE: All file/folder paths should be relative to the project root. The
+ * absolute paths should be resolved during runtime by our build internal/server.
+ */
+
+import * as EnvVars from './utils/envVars';
+
+const values = {
+ // The configuration values that should be exposed to our client bundle.
+ // This value gets passed through the /shared/utils/objects/filterWithRules
+ // util to create a filter object that can be serialised and included
+ // with our client bundle.
+ clientConfigFilter: {
+ // This is here as an example showing that you can expose variables
+ // that were potentially provivded by the environment
+ welcomeMessage: true,
+ graphqlUri: true,
+ graphqlBatch: true,
+ // We only need to expose the enabled flag of the service worker.
+ serviceWorker: {
+ enabled: true,
+ },
+ // We need to expose all the polyfill.io settings.
+ polyfillIO: true,
+ // We need to expose all the htmlPage settings.
+ htmlPage: true,
+ },
+
+ // The host on which the server should run.
+ host: EnvVars.string('HOST', '0.0.0.0'),
+ // The port on which the server should run.
+ port: EnvVars.number('PORT', 1337),
+ graphqlUri: EnvVars.string('GRAPHQL_URI', 'http://localhost:1337/graphql'),
+ graphqlBatch: false,
+ // The port on which the client bundle development server should run.
+ clientDevServerPort: EnvVars.number('CLIENT_DEV_PORT', 7331),
+
+ // This is an example environment variable which is used within the react
+ // application to demonstrate the usage of environment variables across
+ // the client and server bundles.
+ welcomeMessage: EnvVars.string('WELCOME_MSG', 'Hello world!'),
+
+ // Disable server side rendering?
+ disableSSR: false,
+
+ // How long should we set the browser cache for the served assets?
+ // Don't worry, we add hashes to the files, so if they change the new files
+ // will be served to browsers.
+ // We are using the "ms" format to set the length.
+ // @see https://www.npmjs.com/package/ms
+ browserCacheMaxAge: '365d',
+
+ // We use the polyfill.io service which provides the polyfills that a
+ // client needs, which is far more optimal than the large output
+ // generated by babel-polyfill.
+ // Note: we have to keep this seperate from our "htmlPage" configuration
+ // as the polyfill needs to be loaded BEFORE any of our other javascript
+ // gets parsed.
+ polyfillIO: {
+ enabled: true,
+ url: '//cdn.polyfill.io/v2/polyfill.min.js',
+ // Reference https://qa.polyfill.io/v2/docs/features for a full list
+ // of features.
+ features: [
+ // The default list.
+ 'default',
+ 'es6',
+ ],
+ },
+
+ // Basic configuration for the HTML page that hosts our application.
+ // We make use of react-helmet to consume the values below.
+ // @see https://github.com/nfl/react-helmet
+ htmlPage: {
+ titleTemplate: 'React, Universally - %s',
+ defaultTitle: 'React, Universally',
+ description:
+ 'A starter kit giving you the minimum requirements for a production ready universal react application.',
+ },
+
+ // Content Security Policy (CSP)
+ // @see server/middleware/security for more info.
+ cspExtensions: {
+ childSrc: [],
+ connectSrc: [],
+ defaultSrc: [],
+ fontSrc: ['fonts.googleapis.com/css', 'fonts.gstatic.com'],
+ imgSrc: [],
+ mediaSrc: [],
+ manifestSrc: [],
+ objectSrc: [],
+ scriptSrc: [
+ // Allow scripts from cdn.polyfill.io so that we can import the
+ // polyfill.
+ 'cdn.polyfill.io',
+ 'cdn.jsdelivr.net',
+ "'unsafe-inline'",
+ ],
+ styleSrc: [
+ 'cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css',
+ 'fonts.googleapis.com/css',
+ 'cdn.jsdelivr.net',
+ ],
+ },
+
+ // Path to the public assets that will be served off the root of the
+ // HTTP server.
+ publicAssetsPath: './public',
+
+ // Where does our build output live?
+ buildOutputPath: './build',
+
+ // Do you want to included source maps for optimised builds of the client
+ // bundle?
+ includeSourceMapsForOptimisedClientBundle: false,
+
+ // These extensions are tried when resolving src files for our bundles..
+ bundleSrcTypes: ['js', 'jsx', 'json'],
+
+ // What should we name the json output file that webpack generates
+ // containing details of all output files for a bundle?
+ bundleAssetsFileName: 'assets.json',
+
+ // node_modules are not included in any bundles that target "node" as a
+ // runtime (e.g.. the server bundle) as including them often breaks builds
+ // due to thinks like require statements containing expressions..
+ // However. some of the modules contain files need to be processed by
+ // one of our Webpack loaders (e.g. CSS). Add any file types to the list
+ // below to allow them to be processed by Webpack.
+ nodeExternalsFileTypeWhitelist: [
+ /\.(eot|woff|woff2|ttf|otf)$/,
+ /\.(svg|png|jpg|jpeg|gif|ico)$/,
+ /\.(mp4|mp3|ogg|swf|webp)$/,
+ /\.(css|scss|sass|sss|less)$/,
+ ],
+
+ // Note: you can only have a single service worker instance. Our service
+ // worker implementation is bound to the "client" and "server" bundles.
+ // It includes the "client" bundle assets, as well as the public folder assets,
+ // and it is served by the "server" bundle.
+ serviceWorker: {
+ // Enabled?
+ enabled: true,
+ // Service worker name
+ fileName: 'sw.js',
+ // Paths to the public assets which should be included within our
+ // service worker. Relative to our public folder path, and accepts glob
+ // syntax.
+ includePublicAssets: [
+ // NOTE: This will include ALL of our public folder assets. We do
+ // a glob pull of them and then map them to /foo paths as all the
+ // public folder assets get served off the root of our application.
+ // You may or may not want to be including these assets. Feel free
+ // to remove this or instead include only a very specific set of
+ // assets.
+ './**/*',
+ ],
+ // Offline page file name.
+ offlinePageFileName: 'offline.html',
+ },
+
+ bundles: {
+ client: {
+ // Src entry file.
+ srcEntryFile: './client/index.js',
+
+ // Src paths.
+ srcPaths: [
+ './client',
+ './shared',
+ // The service worker offline page generation needs access to the
+ // config folder. Don't worry we have guards within the config files
+ // to ensure they never get included in a client bundle.
+ './config',
+ ],
+
+ // Where does the client bundle output live?
+ outputPath: './build/client',
+
+ // What is the public http path at which we must serve the bundle from?
+ webPath: '/client/',
+
+ // Configuration settings for the development vendor DLL. This will be created
+ // by our development server and provides an improved dev experience
+ // by decreasing the number of modules that webpack needs to process
+ // for every rebuild of our client bundle. It by default uses the
+ // dependencies configured in package.json however you can customise
+ // which of these dependencies are excluded, whilst also being able to
+ // specify the inclusion of additional modules below.
+ devVendorDLL: {
+ // Enabled?
+ enabled: true,
+
+ // Specify any dependencies that you would like to include in the
+ // Vendor DLL.
+ //
+ // NOTE: It is also possible that some modules require specific
+ // webpack loaders in order to be processed (e.g. CSS/SASS etc).
+ // For these cases you don't want to include them in the Vendor DLL.
+ include: [
+ 'react-async-component',
+ 'react',
+ 'react-dom',
+ 'react-helmet',
+ 'react-router-dom',
+ 'redux',
+ 'react-redux',
+ 'redux-thunk',
+ 'axios',
+ 'apollo-client',
+ 'react-apollo',
+ 'graphql-tag',
+ 'graphql-tools',
+ ],
+
+ // The name of the vendor DLL.
+ name: '__dev_vendor_dll__',
+ },
+ },
+
+ server: {
+ // Src entry file.
+ srcEntryFile: './server/index.js',
+
+ // Src paths.
+ srcPaths: ['./server', './shared', './config'],
+
+ // Where does the server bundle output live?
+ outputPath: './build/server',
+ },
+ },
+
+ additionalNodeBundles: {
+ // NOTE: The webpack configuration and build scripts have been built so
+ // that you can add arbitrary additional node bundle configurations here.
+ //
+ // A common requirement for larger projects is to add additional "node"
+ // target bundles (e.g an APi server endpoint). Therefore flexibility has been
+ // baked into our webpack config factory to allow for this.
+ //
+ // Simply define additional configurations similar to below. The development
+ // server will manage starting them up for you. The only requirement is that
+ // within the entry for each bundle you create and return the "express"
+ // listener.
+ /*
+ apiServer: {
+ srcEntryFile: './api/index.js',
+ srcPaths: [
+ './api',
+ './shared',
+ './config',
+ ],
+ outputPath: './build/api',
+ }
+ */
+ },
+
+ // These plugin definitions provide you with advanced hooks into customising
+ // the project without having to reach into the internals of the tools.
+ //
+ // We have decided to create this plugin approach so that you can come to
+ // a centralised configuration folder to do most of your application
+ // configuration adjustments. Additionally it helps to make merging
+ // from the origin starter kit a bit easier.
+ plugins: {
+ // This plugin allows you to provide final adjustments your babel
+ // configurations for each bundle before they get processed.
+ //
+ // This function will be called once for each for your bundles. It will be
+ // provided the current webpack config, as well as the buildOptions which
+ // detail which bundle and mode is being targetted for the current function run.
+ babelConfig: (babelConfig, buildOptions) => {
+ // eslint-disable-next-line no-unused-vars
+ const { target, mode } = buildOptions;
+
+ // Example
+ /*
+ if (target === 'server' && mode === 'development') {
+ babelConfig.presets.push('foo');
+ }
+ */
+
+ return babelConfig;
+ },
+
+ // This plugin allows you to provide final adjustments your webpack
+ // configurations for each bundle before they get processed.
+ //
+ // I would recommend looking at the "webpack-merge" module to help you with
+ // merging modifications to each config.
+ //
+ // This function will be called once for each for your bundles. It will be
+ // provided the current webpack config, as well as the buildOptions which
+ // detail which bundle and mode is being targetted for the current function run.
+ webpackConfig: (webpackConfig, buildOptions) => {
+ // eslint-disable-next-line no-unused-vars
+ const { target, mode } = buildOptions;
+
+ // Example:
+ /*
+ if (target === 'server' && mode === 'development') {
+ webpackConfig.plugins.push(new MyCoolWebpackPlugin());
+ }
+ */
+
+ // Debugging/Logging Example:
+ /*
+ if (target === 'server') {
+ console.log(JSON.stringify(webpackConfig, null, 4));
+ }
+ */
+
+ return webpackConfig;
+ },
+ },
+};
+
+// This protects us from accidentally including this configuration in our
+// client bundle. That would be a big NO NO to do. :)
+if (process.env.BUILD_FLAG_IS_CLIENT === 'true') {
+ throw new Error(
+ "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`.",
+ );
+}
+
+export default values;
diff --git a/docs/APPLICATION_CONFIG.md b/docs/APPLICATION_CONFIG.md
deleted file mode 100644
index 5c6582df..00000000
--- a/docs/APPLICATION_CONFIG.md
+++ /dev/null
@@ -1,156 +0,0 @@
- - [Project Overview](/docs/PROJECT_OVERVIEW.md)
- - __[Application Configuration](/docs/APPLICATION_CONFIG.md)__
- - [Package Script Commands](/docs/PKG_SCRIPTS.md)
- - [Feature Branches](/docs/FEATURE_BRANCHES.md)
- - [Deploy your very own Server Side Rendering React App in 5 easy steps](/docs/DEPLOY_TO_NOW.md)
- - [FAQ](/docs/FAQ.md)
-
-# Application configuration
-
-The application configuration has been centralised to live within the `/config/index.js` file.
-
-Just about everything that should be reasonably configurable will be contained within here. It even contains plugin function definitions that allow you to extend/modify the Babel and Webpack configurations.
-
-## TOC
-
- - [Goals](#goals)
- - [Background](#background)
- - [Managing Configuration](#managing-configuration)
- - [Defining the configuration values safe for client bundles](#defining-the-configuration-values-safe-for-client-bundles)
- - [Environment Values](#environment-values)
- - [Reading Configuration](#reading-configuration)
- - [In the "server" or "tools" source](#in-the-server-or-tools-source)
- - [In the "client" or "shared" folders](#in-the-client-or-shared-folders)
- - [Config Highlights](#config-highlights)
- - [Easily add an "API" bundle](#easily-add-an-api-bundle)
-
-## Goals
-
-The goals of our application configuration are:
-
- - Easy to use
- - Centralised
- - Secure
- - Allows for configuration to be provided at build and execution time
-
-## Background
-
-Below are some of the problems that we faced, and how we ended up with our current implementation...
-
-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 user's browsers therefore you have to be extra careful in what you include in the bundle. Webpack by default bundles all code together if it is imported within your source. Therefore if you were to import the application configuration within a module that will be included in the "client" bundle, 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.
-
-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, this solution fails to address our desire to be able to expose execution time provided values (e.g. `FOO=bar yarn run start`) to our client bundle. These environment variables can only be interpreted at runtime, 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. This would then allow us to ensure that environment variables could be properly exposed. This works well, however, it introduces a new problem: As most of our code is in the "shared" folder you are forced to put in boilerplate code that will read the application configuration from either the `window.__CLIENT_CONFIG__` or the "config" file depending on which bundle is being built (i.e. "client" or "server"). This isn't a trivial process and is easy to get wrong.
-
-So now we had two problems to deal with:
- 1. Prevent the accidental import of the configuration object into client bundles.
- 2. Provide an abstraction to the boilerplate in order to read configuration values in shared source code.
-
-###ย Problem 1: Guarding import of the config object into client bundles.
-
-Because we now state that our application configuration for client bundles should be a filtered object that is bound to the `window.__CLIENT_CONFIG__` within the HTTP response this problem became quite trivial to solve. Within our `./config` file we simply put a guarded check that uses the `process.env.IS_CLIENT` flag that is provided by the Webpack `DefinePlugin`. This boolean flag indicates whether Webpack is bundling a "client" bundle or not. So if this flag is `true` we throw an error stating that this is a dangerous move. This is a build time error.
-
-### Problem 2: Abstracting access to either `window.__CLIENT_CONFIG__` or `./config`
-
-For this we created a helper function get `safeConfigGet`. It is located in `./src/shared/utils/config`. You can use it like so:
-
-```js
-import { safeConfigGet } from '../shared/utils/config';
-
-export function MyComponent() {
- return
{safeConfigGet(['welcomeMessage'])}
;
-}
-```
-
-You must use this helper function any time you need to access configuration within the "shared" src folder. We also recommend that you use it within any "client" source too (you could just use the `window.__CLIENT_CONFIG__` object in this case, but it is nice to keep the config access as familiar as possible throughout your source).
-
-This does all the abstraction required, and will make sure that "problem 1" detailed above isn't hit either.
-
-## Managing Configuration
-
-ALL configuration should be added/managed to the `./config/index.js` file. We even recommend that you attach environment read variables as properties to this configuration file in order to provide a familiar read API throughout your source.
-
-### Defining the configuration values safe for client bundles
-
-Within the bottom of the `./config/index.js` you will see that a `clientConfig` value gets exported. This configuration value is created by providing a set of rules/filters that detail which of the configuration values you deem safe/required for inclusion in your client bundles. Please go to this section of the configuration file for more detail on how this filtering mechanism works.
-
-This `clientConfig` export will be serialised and attached to the `window.__CLIENT_CONFIG__` by the `reactApplication` middleware within the HTML response it returns.
-
-## Environment Values
-
-Environment specific values are support via host system environment variables (e.g. `FOO=bar yarn run start`) and/or by providing an "env" file.
-
-"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).
-
-> 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.
-
-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.
-
-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`.
-
-Then when you run your code with the `NODE_ENV=target` set it will load the appropriate "env.target" file.
-
- > Note: if an environment specific configuration file exists, it will be used over the more generic `.env` file.
-
-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 the `.env` file, and others via the cli/host (e.g. `FOO=bar yarn 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.
-
-> 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.
-
-## Reading Configuration
-
-### In the "server" or "tools" source
-
-Within the server or build tools it is safe to just import and use the configuration file directly.
-
-```js
-import config from '../../config';
-
-// ... code bootstrapping an express instance ...
-
-app.listen(config.port, () => console.log('Server started.'));
-```
-
-As stated in the background section above you must not import and use the config file in this manner within your "shared" source, however, don't worry about it as you will get a build time error if you accidentally did so. The error will also include details on the proper API that you should use for the "shared" source.
-
-### In the "client" or "shared" folders
-
-You can't import the `./config` file in the "client" or "shared" source as this will cause build failures. The configuration object will be bound to `window.__CLIENT_CONFIG__` as detailed in the background section above. Therefore to access the configuration within these cases we recommend the use of our provided helper located in `./src/shared/utils/config`.
-
-```js
-import { safeConfigGet } from '../shared/utils/config';
-
-export function MyComponent() {
- return
{safeConfigGet(['welcomeMessage'])}
;
-}
-```
-
-The `window.__CLIENT_CONFIG__` will have the same structure as the original `./config`, however, it will only contain a subset of it (i.e. only the values you deemed safe for inclusion within the client).
-
-Our `safeConfigGet` allows you to specify nested path structures in the form of an array. Say for example you wanted to access a configuration in a similar manner to the following:
-
-```js
-import config from '../../config';
-
-console.log(config.serviceWorker.enabled);
-```
-
-You can't use the above in the "shared" or "client" code, you have to use our `safeConfigGet` helper. You would access the same value like so:
-
-```js
-import { safeConfigGet } from '../shared/utils/config';
-
-console.log(safeConfigGet(['serviceWorker', 'enabled']));
-```
-
-The `safeConfigGet` 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.
-
-## Config Highlights
-
-Below are some interesting aspects of the configuration file to be aware of.
-
-### Easily add an "API" bundle
-
-A fairly common requirement for a project that scales is to create additional servers bundles, e.g. an API server.
-
-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.
-
-_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.
diff --git a/tools/.eslintrc b/internal/.eslintrc
similarity index 100%
rename from tools/.eslintrc
rename to internal/.eslintrc
diff --git a/tools/development/createVendorDLL.js b/internal/development/createVendorDLL.js
similarity index 89%
rename from tools/development/createVendorDLL.js
rename to internal/development/createVendorDLL.js
index 70fcabf8..10e39bec 100644
--- a/tools/development/createVendorDLL.js
+++ b/internal/development/createVendorDLL.js
@@ -7,9 +7,8 @@ import config from '../../config';
import { log } from '../utils';
function createVendorDLL(bundleName, bundleConfig) {
- const dllConfig = config.bundles.client.devVendorDLL;
+ const dllConfig = config('bundles.client.devVendorDLL');
- // $FlowFixMe
const pkg = require(pathResolve(appRootDir.get(), './package.json'));
const devDLLDependencies = dllConfig.include.sort();
@@ -17,14 +16,14 @@ function createVendorDLL(bundleName, bundleConfig) {
// We calculate a hash of the package.json's dependencies, which we can use
// to determine if dependencies have changed since the last time we built
// the vendor dll.
- const currentDependenciesHash = md5(JSON.stringify(
- devDLLDependencies.map(dep =>
+ const currentDependenciesHash = md5(
+ JSON.stringify(
+ devDLLDependencies.map(dep => [dep, pkg.dependencies[dep], pkg.devDependencies[dep]]),
// We do this to include any possible version numbers we may have for
// a dependency. If these change then our hash should too, which will
// result in a new dev dll build.
- [dep, pkg.dependencies[dep], pkg.devDependencies[dep]],
),
- ));
+ );
const vendorDLLHashFilePath = pathResolve(
appRootDir.get(),
@@ -46,11 +45,7 @@ function createVendorDLL(bundleName, bundleConfig) {
},
plugins: [
new webpack.DllPlugin({
- path: pathResolve(
- appRootDir.get(),
- bundleConfig.outputPath,
- `./${dllConfig.name}.json`,
- ),
+ path: pathResolve(appRootDir.get(), bundleConfig.outputPath, `./${dllConfig.name}.json`),
name: dllConfig.name,
}),
],
@@ -72,7 +67,7 @@ function createVendorDLL(bundleName, bundleConfig) {
reject(err);
return;
}
- // Update the dependency hash
+ // Update the dependency hash
fs.writeFileSync(vendorDLLHashFilePath, currentDependenciesHash);
resolve();
diff --git a/tools/development/hotClientServer.js b/internal/development/hotClientServer.js
similarity index 89%
rename from tools/development/hotClientServer.js
rename to internal/development/hotClientServer.js
index e6580447..e7d7eff3 100644
--- a/tools/development/hotClientServer.js
+++ b/internal/development/hotClientServer.js
@@ -2,6 +2,7 @@ import express from 'express';
import createWebpackMiddleware from 'webpack-dev-middleware';
import createWebpackHotMiddleware from 'webpack-hot-middleware';
import ListenerManager from './listenerManager';
+import config from '../../config';
import { log } from '../utils';
class HotClientServer {
@@ -23,7 +24,7 @@ class HotClientServer {
quiet: true,
noInfo: true,
headers: {
- 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Origin': `http://${config('host')}:${config('port')}`,
},
// Ensure that the public path is taken from the compiler webpack config
// as it will have been created as an absolute path to avoid conflicts
@@ -34,7 +35,7 @@ class HotClientServer {
app.use(this.webpackDevMiddleware);
app.use(createWebpackHotMiddleware(compiler));
- const listener = app.listen(port, host);
+ const listener = app.listen(port);
this.listenerManager = new ListenerManager(listener, 'client');
@@ -69,9 +70,7 @@ class HotClientServer {
dispose() {
this.webpackDevMiddleware.close();
- return this.listenerManager
- ? this.listenerManager.dispose()
- : Promise.resolve();
+ return this.listenerManager ? this.listenerManager.dispose() : Promise.resolve();
}
}
diff --git a/tools/development/hotDevelopment.js b/internal/development/hotDevelopment.js
similarity index 58%
rename from tools/development/hotDevelopment.js
rename to internal/development/hotDevelopment.js
index a82da566..6527a0e9 100644
--- a/tools/development/hotDevelopment.js
+++ b/internal/development/hotDevelopment.js
@@ -35,14 +35,11 @@ const initializeBundle = (name, bundleConfig) => {
// Install the vendor DLL plugin.
webpackConfig.plugins.push(
new webpack.DllReferencePlugin({
- // $FlowFixMe
- manifest: require(
- pathResolve(
- appRootDir.get(),
- bundleConfig.outputPath,
- `${bundleConfig.devVendorDLL.name}.json`,
- ),
- ),
+ manifest: require(pathResolve(
+ appRootDir.get(),
+ bundleConfig.outputPath,
+ `${bundleConfig.devVendorDLL.name}.json`,
+ )),
}),
);
}
@@ -58,62 +55,61 @@ const initializeBundle = (name, bundleConfig) => {
throw err;
}
};
+
return { name, bundleConfig, createCompiler };
};
class HotDevelopment {
-
-
constructor() {
this.hotClientServer = null;
this.hotNodeServers = [];
- const clientBundle = initializeBundle('client', config.bundles.client);
+ const clientBundle = initializeBundle('client', config('bundles.client'));
- const nodeBundles = [initializeBundle('server', config.bundles.server)]
- .concat(Object.keys(config.additionalNodeBundles).map(name =>
- initializeBundle(name, config.additionalNodeBundles[name]),
- ));
+ const nodeBundles = [initializeBundle('server', config('bundles.server'))].concat(
+ Object.keys(config('additionalNodeBundles')).map(name =>
+ initializeBundle(name, config('additionalNodeBundles')[name]),
+ ),
+ );
- Promise
+ Promise.resolve(
// First ensure the client dev vendor DLLs is created if needed.
- .resolve(
- usesDevVendorDLL(config.bundles.client)
- ? createVendorDLL('client', config.bundles.client)
- : true,
- )
+ usesDevVendorDLL(config('bundles.client'))
+ ? createVendorDLL('client', config('bundles.client'))
+ : true,
+ )
// Then start the client development server.
.then(
- () => new Promise((resolve) => {
- const { createCompiler } = clientBundle;
- const compiler = createCompiler();
- compiler.plugin('done', (stats) => {
- if (!stats.hasErrors()) {
- resolve(compiler);
- }
- });
- this.hotClientServer = new HotClientServer(compiler);
- }),
+ () =>
+ new Promise((resolve) => {
+ const { createCompiler } = clientBundle;
+ const compiler = createCompiler();
+ compiler.plugin('done', (stats) => {
+ if (!stats.hasErrors()) {
+ resolve(compiler);
+ }
+ });
+ this.hotClientServer = new HotClientServer(compiler);
+ }),
vendorDLLsFailed,
)
// Then start the node development server(s).
.then((clientCompiler) => {
- this.hotNodeServers = nodeBundles
- .map(({ name, createCompiler }) =>
- // $FlowFixMe
- new HotNodeServer(name, createCompiler(), clientCompiler),
- );
+ this.hotNodeServers = nodeBundles.map(
+ ({ name, createCompiler }) => new HotNodeServer(name, createCompiler(), clientCompiler),
+ );
});
}
dispose() {
- const safeDisposer = server =>
- (server ? server.dispose() : Promise.resolve());
+ const safeDisposer = server => (server ? server.dispose() : Promise.resolve());
// First the hot client server.
- return safeDisposer(this.hotClientServer)
- // Then dispose the hot node server(s).
- .then(() => Promise.all(this.hotNodeServers.map(safeDisposer)));
+ return (
+ safeDisposer(this.hotClientServer)
+ // Then dispose the hot node server(s).
+ .then(() => Promise.all(this.hotNodeServers.map(safeDisposer)))
+ );
}
}
diff --git a/tools/development/hotNodeServer.js b/internal/development/hotNodeServer.js
similarity index 95%
rename from tools/development/hotNodeServer.js
rename to internal/development/hotNodeServer.js
index d8d1a46a..7129d2e2 100644
--- a/tools/development/hotNodeServer.js
+++ b/internal/development/hotNodeServer.js
@@ -22,7 +22,7 @@ class HotNodeServer {
});
}
- const newServer = spawn('node', [compiledEntryFile]);
+ const newServer = spawn('node', [compiledEntryFile, '--color']);
log({
title: name,
@@ -119,7 +119,9 @@ class HotNodeServer {
this.watcher.close(resolve);
});
- return stopWatcher.then(() => { if (this.server) this.server.kill(); });
+ return stopWatcher.then(() => {
+ if (this.server) this.server.kill();
+ });
}
}
diff --git a/tools/development/index.js b/internal/development/index.js
similarity index 92%
rename from tools/development/index.js
rename to internal/development/index.js
index 7d8bea8d..44896298 100644
--- a/tools/development/index.js
+++ b/internal/development/index.js
@@ -8,9 +8,10 @@ let devServer = new HotDevelopment();
// Any changes to our webpack bundleConfigs should restart the development devServer.
const watcher = chokidar.watch([
- pathResolve(appRootDir.get(), 'tools'),
+ pathResolve(appRootDir.get(), 'internal'),
pathResolve(appRootDir.get(), 'config'),
]);
+
watcher.on('ready', () => {
watcher.on('change', () => {
log({
@@ -23,7 +24,7 @@ watcher.on('ready', () => {
Object.keys(require.cache).forEach((modulePath) => {
if (modulePath.indexOf('config') !== -1) {
delete require.cache[modulePath];
- } else if (modulePath.indexOf('tools') !== -1) {
+ } else if (modulePath.indexOf('internal') !== -1) {
delete require.cache[modulePath];
}
});
diff --git a/tools/development/listenerManager.js b/internal/development/listenerManager.js
similarity index 100%
rename from tools/development/listenerManager.js
rename to internal/development/listenerManager.js
diff --git a/internal/docs/ADDING_AN_API_BUNDLE.md b/internal/docs/ADDING_AN_API_BUNDLE.md
new file mode 100644
index 00000000..b643c2b3
--- /dev/null
+++ b/internal/docs/ADDING_AN_API_BUNDLE.md
@@ -0,0 +1,7 @@
+# Adding an "API" Bundle
+
+A fairly common requirement for a project that scales is to create additional servers bundles, e.g. an API server.
+
+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.
+
+_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.
diff --git a/docs/DEPLOY_TO_NOW.md b/internal/docs/DEPLOY_TO_NOW.md
similarity index 60%
rename from docs/DEPLOY_TO_NOW.md
rename to internal/docs/DEPLOY_TO_NOW.md
index 568ad143..54405bc4 100644
--- a/docs/DEPLOY_TO_NOW.md
+++ b/internal/docs/DEPLOY_TO_NOW.md
@@ -1,11 +1,11 @@
- - [Project Overview](/docs/PROJECT_OVERVIEW.md)
- - [Application Configuration](/docs/APPLICATION_CONFIG.md)
- - [Package Script Commands](/docs/PKG_SCRIPTS.md)
- - [Feature Branches](/docs/FEATURE_BRANCHES.md)
- - __[Deploy your very own Server Side Rendering React App in 5 easy steps](/docs/DEPLOY_TO_NOW.md)__
- - [FAQ](/docs/FAQ.md)
+ - [Project Overview](/internal/docs/PROJECT_OVERVIEW.md)
+ - [Project Configuration](/internal/docs/PROJECT_CONFIG.md)
+ - [Package Script Commands](/internal/docs/PKG_SCRIPTS.md)
+ - [Feature Branches](/internal/docs/FEATURE_BRANCHES.md)
+ - __[Deploy your very own Server Side Rendering React App in 5 easy steps](/internal/docs/DEPLOY_TO_NOW.md)__
+ - [FAQ](/internal/docs/FAQ.md)
-# Deploy your very own "React, Universally" App in 4 easy steps
+# Deploy your very own "React, Universally" App in 5 easy steps
__Step 1: Clone the repository.__
@@ -31,4 +31,8 @@ __Step 5: Deploy to "now"__
yarn run deploy
+Or, if you aren't using [`yarn`](https://yarnpkg.com/):
+
+ npm run deploy
+
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)
diff --git a/docs/FAQ.md b/internal/docs/FAQ.md
similarity index 65%
rename from docs/FAQ.md
rename to internal/docs/FAQ.md
index d96d85fa..c00db6b7 100644
--- a/docs/FAQ.md
+++ b/internal/docs/FAQ.md
@@ -1,9 +1,9 @@
- - [Project Overview](/docs/PROJECT_OVERVIEW.md)
- - [Application Configuration](/docs/APPLICATION_CONFIG.md)
- - [Package Script Commands](/docs/PKG_SCRIPTS.md)
- - [Feature Branches](/docs/FEATURE_BRANCHES.md)
- - [Deploy your very own Server Side Rendering React App in 5 easy steps](/docs/DEPLOY_TO_NOW.md)
- - __[FAQ](/docs/FAQ.md)__
+ - [Project Overview](/internal/docs/PROJECT_OVERVIEW.md)
+ - [Project Configuration](/internal/docs/PROJECT_CONFIG.md)
+ - [Package Script Commands](/internal/docs/PKG_SCRIPTS.md)
+ - [Feature Branches](/internal/docs/FEATURE_BRANCHES.md)
+ - [Deploy your very own Server Side Rendering React App in 5 easy steps](/internal/docs/DEPLOY_TO_NOW.md)
+ - __[FAQ](/internal/docs/FAQ.md)__
# Frequently Asked Questions
@@ -15,37 +15,6 @@ If you perform build tasks on your production environment you must ensure that y
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.
-___Q:___ __After adding a module that contains SASS/CSS (e.g. material-ui or bootstrap) the hot development server fails__
-
-The development server has been configured to automatically generate a "Vendor DLL" containing all the modules that are used in your source. We do this so that any rebuilds by Webpack are optimised as it need not bundle all your project's dependencies every time. This works great most of the time, however, if you introduce a module that depends on one of your Webpack loaders (e.g. CSS/Images) then you need to make sure that you add the respective module to the vendor DLL ignores list within your project configuration.
-
-For example, say you added `bootstrap` and were referencing the CSS file like so in your client bundle:
-
-```js
-import 'bootstrap/dist/css/bootstrap.css';
-```
-
-You would then need to edit `./config/private/project.js` and make the following adjustment:
-
-```js
-export default {
- ...
- bundles: {
- client: {
- ...,
- devVendorDLL: {
- ...,
- ignores: ['bootstrap/dist/css/bootstrap.css']
- }
- },
- ...
- }
- ...
-}
-```
-
-This ensures that the respective import will be ignored when generating the development "Vendor DLL" which means it will get processed by Webpack and included successfully in your project.
-
___Q:___ __My project fails to build and execute when I deploy it to my host__
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.
@@ -93,3 +62,9 @@ git merge upstream/master
# Deal with the merge conflicts, delete the yarn.lock file and
# rebuild it, then commit and push.
```
+
+___Q:___ __My development server starts and bundles correctly, but the JavaScript bundles don't load. What causes this to happen?__
+
+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.
+
+
\ No newline at end of file
diff --git a/docs/FEATURE_BRANCHES.md b/internal/docs/FEATURE_BRANCHES.md
similarity index 70%
rename from docs/FEATURE_BRANCHES.md
rename to internal/docs/FEATURE_BRANCHES.md
index 5ce1554e..e20010fc 100644
--- a/docs/FEATURE_BRANCHES.md
+++ b/internal/docs/FEATURE_BRANCHES.md
@@ -1,26 +1,18 @@
- - [Project Overview](/docs/PROJECT_OVERVIEW.md)
- - [Application Configuration](/docs/APPLICATION_CONFIG.md)
- - [Package Script Commands](/docs/PKG_SCRIPTS.md)
- - __[Feature Branches](/docs/FEATURE_BRANCHES.md)__
- - [Deploy your very own Server Side Rendering React App in 5 easy steps](/docs/DEPLOY_TO_NOW.md)
- - [FAQ](/docs/FAQ.md)
+ - [Project Overview](/internal/docs/PROJECT_OVERVIEW.md)
+ - [Project Configuration](/internal/docs/PROJECT_CONFIG.md)
+ - [Package Script Commands](/internal/docs/PKG_SCRIPTS.md)
+ - __[Feature Branches](/internal/docs/FEATURE_BRANCHES.md)__
+ - [Deploy your very own Server Side Rendering React App in 5 easy steps](/internal/docs/DEPLOY_TO_NOW.md)
+ - [FAQ](/internal/docs/FAQ.md)
# Feature Branches
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.
- [`apollo`](https://github.com/ctrlplusb/react-universally/tree/feature/apollo) - Adds the Apollo Stack (i.e. Graphql).
- - [`flow`](https://github.com/andreyluiz/react-universally/tree/feature/flow) - Adds static type checking using Flow.
- - [`found`](https://github.com/andreyluiz/react-universally/tree/feature/found) - Adds the Found router in replacement to react-router.
- - [`glamor`](https://github.com/ctrlplusb/react-universally/tree/feature/glamor) - Adds the Glamor CSS-in-JS library.
- - [`koa2`](https://github.com/ctrlplusb/react-universally/tree/feature/koa2) - Replaces Express with Koa2.
- - [`jest`](https://github.com/ctrlplusb/react-universally/tree/feature/jest) - Adds the Jest testing framework.
- [`mobx`](https://github.com/andreyluiz/react-universally/tree/feature/mobx) - Adds MobX as a state management library.
- - [`preact`](https://github.com/andreyluiz/react-universally/tree/feature/preact) - Replaces React with Preact via `preact-compat` a React polyfill that uses Preact under the hood. Smaller, faster.
- [`postcss-sass`](https://github.com/ctrlplusb/react-universally/tree/feature/postcss-sass) - Adds PostCSS and SASS.
- [`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. It also merges in the `flow` feature branch.
- - [`styled-components`](https://github.com/ctrlplusb/react-universally/tree/feature/styled-components) - Adds the Styled Components CSS-in-JS library.
- - [`styletron`](https://github.com/ctrlplusb/react-universally/tree/feature/styletron) - Adds the Styletron CSS-in-JS library.
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. :)
diff --git a/docs/PKG_SCRIPTS.md b/internal/docs/PKG_SCRIPTS.md
similarity index 56%
rename from docs/PKG_SCRIPTS.md
rename to internal/docs/PKG_SCRIPTS.md
index 34e28337..69ea99c4 100644
--- a/docs/PKG_SCRIPTS.md
+++ b/internal/docs/PKG_SCRIPTS.md
@@ -1,23 +1,27 @@
- - [Project Overview](/docs/PROJECT_OVERVIEW.md)
- - [Application Configuration](/docs/APPLICATION_CONFIG.md)
- - __[Package Script Commands](/docs/PKG_SCRIPTS.md)__
- - [Feature Branches](/docs/FEATURE_BRANCHES.md)
- - [Deploy your very own Server Side Rendering React App in 5 easy steps](/docs/DEPLOY_TO_NOW.md)
- - [FAQ](/docs/FAQ.md)
+ - [Project Overview](/internal/docs/PROJECT_OVERVIEW.md)
+ - [Project Configuration](/internal/docs/PROJECT_CONFIG.md)
+ - __[Package Script Commands](/internal/docs/PKG_SCRIPTS.md)__
+ - [Feature Branches](/internal/docs/FEATURE_BRANCHES.md)
+ - [Deploy your very own Server Side Rendering React App in 5 easy steps](/internal/docs/DEPLOY_TO_NOW.md)
+ - [FAQ](/internal/docs/FAQ.md)
# Package Scripts
-## `yarn run development`
+## `yarn run analyze:client`
-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.
+Creates an 'webpack-bundle-analyze' session against the production build of the client bundle.
+
+## `yarn run analyze:server`
+
+Creates an 'webpack-bundle-analyze' session against the production build of the server bundle.
## `yarn run build`
-Builds the client and server bundles, with the output being production optimised.
+Builds the client and server bundles, with the output being optimized.
-## `yarn run start`
+## `yarn run build:dev`
-Executes the server. It expects you to have already built the bundles either via the `yarn run build` command or manually.
+Builds the client and server bundles, with the output including development related code.
## `yarn run clean`
@@ -27,13 +31,17 @@ Deletes any build output that would have originated from the other commands.
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.
+## `yarn run develop`
+
+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.
+
## `yarn run lint`
-Executes `eslint` (using the Airbnb config) against the src folder. Alternatively you could look to install the `eslint-loader` and integrate it into the `webpack` bundle process.
+Executes `eslint` against the project. Alternatively you could look to install the `eslint-loader` and integrate it into the `webpack` bundle process.
-## `yarn run analyze`
+## `yarn run start`
-Creates an 'webpack-bundle-analyze' session against the production build of the client bundle. This is super handy for figuring out just exactly what dependencies are being included within your bundle. Try clicking around, it's an awesome tool.
+Executes the server. It expects you to have already built the bundles using the `yarn run build` command.
##ย `yarn run test`
diff --git a/internal/docs/PROJECT_CONFIG.md b/internal/docs/PROJECT_CONFIG.md
new file mode 100644
index 00000000..e93aa86c
--- /dev/null
+++ b/internal/docs/PROJECT_CONFIG.md
@@ -0,0 +1,78 @@
+ - [Project Overview](/internal/docs/PROJECT_OVERVIEW.md)
+ - __[Project Configuration](/internal/docs/PROJECT_CONFIG.md)__
+ - [Package Script Commands](/internal/docs/PKG_SCRIPTS.md)
+ - [Feature Branches](/internal/docs/FEATURE_BRANCHES.md)
+ - [Deploy your very own Server Side Rendering React App in 5 easy steps](/internal/docs/DEPLOY_TO_NOW.md)
+ - [FAQ](/internal/docs/FAQ.md)
+
+# Project Configuration
+
+The application configuration has been centralised to live within the `/config` folder.
+
+You read configuration values using the `/config/index.js` helper, and you edit the configuration values in the `/config/values.js` file.
+
+## TOC
+
+ - [Background and Usage](#background-and-usage)
+ - [Declaring the configuration values that are safe for client bundles](#declaring-the-configuration-values-that-are-safe-for-client-bundles)
+ - [Environment Specific Values](#environment-specifc-values)
+
+## Background and Usage
+
+Below are some of the problems that we faced, and how we ended up with our current implementation...
+
+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.
+
+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.
+
+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:
+
+```js
+import config from '../config';
+
+export function MyComponent() {
+ return
{config('welcomeMessage')}
;
+}
+```
+
+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:
+
+```js
+config('messages.welcome');
+config(['messages', 'welcome']);
+```
+
+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.
+
+## Declaring the configuration values that are safe for client bundles
+
+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.
+
+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.
+
+## Environment Specific Values
+
+Environment specific values are support via host system environment variables (e.g. `FOO=bar yarn run start`) and/or by providing an "env" file.
+
+"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).
+
+> 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.
+
+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.
+
+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`.
+
+In order to target a specific environment configuration file you have to provide a matching `DEPLOYMENT` environment variable. For example:
+
+```bash
+yarn run build
+DEPLOYMENT=staging yarn run start # This will look for a .env.staging file
+```
+
+ > 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.
+
+ > Note: if an environment specific configuration file exists, it will be used over the more generic `.env` file.
+
+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 yarn 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.
+
+> 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.
diff --git a/docs/PROJECT_OVERVIEW.md b/internal/docs/PROJECT_OVERVIEW.md
similarity index 61%
rename from docs/PROJECT_OVERVIEW.md
rename to internal/docs/PROJECT_OVERVIEW.md
index 889fe593..ae41c97a 100644
--- a/docs/PROJECT_OVERVIEW.md
+++ b/internal/docs/PROJECT_OVERVIEW.md
@@ -1,9 +1,9 @@
- - __[Project Overview](/docs/PROJECT_OVERVIEW.md)__
- - [Application Configuration](/docs/APPLICATION_CONFIG.md)
- - [Package Script Commands](/docs/PKG_SCRIPTS.md)
- - [Feature Branches](/docs/FEATURE_BRANCHES.md)
- - [Deploy your very own Server Side Rendering React App in 5 easy steps](/docs/DEPLOY_TO_NOW.md)
- - [FAQ](/docs/FAQ.md)
+ - __[Project Overview](/internal/docs/PROJECT_OVERVIEW.md)__
+ - [Project Configuration](/internal/docs/PROJECT_CONFIG.md)
+ - [Package Script Commands](/internal/docs/PKG_SCRIPTS.md)
+ - [Feature Branches](/internal/docs/FEATURE_BRANCHES.md)
+ - [Deploy your very own Server Side Rendering React App in 5 easy steps](/internal/docs/DEPLOY_TO_NOW.md)
+ - [FAQ](/internal/docs/FAQ.md)
# Project Overview
@@ -18,13 +18,15 @@ Below is a general overview of the project.
## Bundled by Webpack
-This starter uses Webpack 2 to produce bundles for both the client and the server. The `tools/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.
+This starter uses Webpack 2 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.
> 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.
## Transpiled by Babel
-It also uses 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 within each separate slice of the project. 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.
+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.
+
+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.
## Security
@@ -46,17 +48,19 @@ Below are some of the critical folders of the project along with a comment descr
```
/
|- config // Centralised project configuration.
+| |- values.js // Configuration values
+| |- index.js // Unified Configuration Reader API
|
|- build // The target output dir for our build commands.
| |- client // The built client module.
| |- server // The built server module.
|
-|- src // All the source code.
-| |- server // The server bundle entry and specific source.
-| |- client // The client bundle entry and specific source.
-| |- shared // The shared code between the bundles.
+|- server // The server bundle entry and specific source.
+|- client // The client bundle entry and specific source.
+|- shared // The shared code between the bundles.
|
-|- tools
+|- internal
+| |- docs // Documentation
| |- development // Development server.
| |- webpack
| |- configFactory.js // Webpack configuration builder.
diff --git a/internal/jest/assetMock.js b/internal/jest/assetMock.js
new file mode 100644
index 00000000..30aa2172
--- /dev/null
+++ b/internal/jest/assetMock.js
@@ -0,0 +1 @@
+module.exports = '/asset/mock';
diff --git a/tools/jest/styleMock.js b/internal/jest/styleMock.js
similarity index 74%
rename from tools/jest/styleMock.js
rename to internal/jest/styleMock.js
index d2191422..e0df15cf 100644
--- a/tools/jest/styleMock.js
+++ b/internal/jest/styleMock.js
@@ -1,3 +1,3 @@
-// tools/test/styleMock.js
+// internal/test/styleMock.js
// Return an object to emulate css modules (if you are using them)
module.exports = {};
diff --git a/internal/scripts/analyze.js b/internal/scripts/analyze.js
new file mode 100644
index 00000000..4fa6477b
--- /dev/null
+++ b/internal/scripts/analyze.js
@@ -0,0 +1,47 @@
+/**
+ * This script creates a webpack stats file on our production build of the
+ * client bundle and then launches the webpack-bundle-analyzer tool allowing
+ * you to easily see what is being included within your bundle.
+ *
+ * @see https://github.com/th0r/webpack-bundle-analyzer
+ */
+
+import webpack from 'webpack';
+import fs from 'fs';
+import { resolve as pathResolve } from 'path';
+import appRootDir from 'app-root-dir';
+import webpackConfigFactory from '../webpack/configFactory';
+import { exec } from '../utils';
+import config from '../../config';
+
+// eslint-disable-next-line no-unused-vars
+const [x, y, ...args] = process.argv;
+const analyzeServer = args.findIndex(arg => arg === '--server') !== -1;
+const analyzeClient = args.findIndex(arg => arg === '--client') !== -1;
+
+let target;
+
+if (analyzeServer) target = 'server';
+else if (analyzeClient) target = 'client';
+else throw new Error('Please specify --server OR --client as target');
+
+const anaylzeFilePath = pathResolve(
+ appRootDir.get(),
+ config('bundles.client.outputPath'),
+ '__analyze__.json',
+);
+
+const clientCompiler = webpack(webpackConfigFactory({ target, optimize: true }));
+
+clientCompiler.run((err, stats) => {
+ if (err) {
+ console.error(err);
+ } else {
+ // Write out the json stats file.
+ fs.writeFileSync(anaylzeFilePath, JSON.stringify(stats.toJson('verbose'), null, 4));
+
+ // Run the bundle analyzer against the stats file.
+ const cmd = `webpack-bundle-analyzer ${anaylzeFilePath} ${config('bundles.client.outputPath')}`;
+ exec(cmd);
+ }
+});
diff --git a/internal/scripts/build.js b/internal/scripts/build.js
new file mode 100644
index 00000000..81479e07
--- /dev/null
+++ b/internal/scripts/build.js
@@ -0,0 +1,34 @@
+/**
+ * This script builds a production output of all of our bundles.
+ */
+
+import webpack from 'webpack';
+import appRootDir from 'app-root-dir';
+import { resolve as pathResolve } from 'path';
+import webpackConfigFactory from '../webpack/configFactory';
+import { exec } from '../utils';
+import config from '../../config';
+
+// eslint-disable-next-line no-unused-vars
+const [x, y, ...args] = process.argv;
+
+const optimize = args.findIndex(arg => arg === '--optimize') !== -1;
+
+// First clear the build output dir.
+exec(`rimraf ${pathResolve(appRootDir.get(), config('buildOutputPath'))}`);
+
+// Get our "fixed" bundle names
+Object.keys(config('bundles'))
+ // And the "additional" bundle names
+ .concat(Object.keys(config('additionalNodeBundles')))
+ // And then build them all.
+ .forEach((bundleName) => {
+ const compiler = webpack(webpackConfigFactory({ target: bundleName, optimize }));
+ compiler.run((err, stats) => {
+ if (err) {
+ console.error(err);
+ return;
+ }
+ console.log(stats.toString({ colors: true }));
+ });
+ });
diff --git a/internal/scripts/clean.js b/internal/scripts/clean.js
new file mode 100644
index 00000000..fa0ce02c
--- /dev/null
+++ b/internal/scripts/clean.js
@@ -0,0 +1,16 @@
+/**
+ * This script removes any exisitng build output.
+ */
+
+import { resolve as pathResolve } from 'path';
+import appRootDir from 'app-root-dir';
+import rimraf from 'rimraf';
+import config from '../../config';
+
+function clean() {
+ rimraf(pathResolve(appRootDir.get(), config('buildOutputPath')), () => {
+ console.log(`Cleaned ${pathResolve(appRootDir.get(), config('buildOutputPath'))}`);
+ });
+}
+
+clean();
diff --git a/tools/scripts/deploy.js b/internal/scripts/deploy.js
similarity index 53%
rename from tools/scripts/deploy.js
rename to internal/scripts/deploy.js
index 6220f1db..4e915e95 100644
--- a/tools/scripts/deploy.js
+++ b/internal/scripts/deploy.js
@@ -1,5 +1,7 @@
-// Deploys to now.
-// @see https://zeit.co/now
+/**
+ * Deploys to now.
+ * @see https://zeit.co/now
+ */
import { exec } from '../utils';
const cmd = 'now';
diff --git a/tools/scripts/preinstall.js b/internal/scripts/preinstall.js
similarity index 83%
rename from tools/scripts/preinstall.js
rename to internal/scripts/preinstall.js
index 8aa98229..bbe02ce5 100644
--- a/tools/scripts/preinstall.js
+++ b/internal/scripts/preinstall.js
@@ -1,10 +1,12 @@
-/* eslint-disable */
+/**
+ * This script will ensure that users are using a supported version of node
+ * for the project.
+ *
+ * NOTE: Ensure this script uses ES5 only as the user may be running an old
+ * version of Node, which this script wants to test against.
+ */
-// NOTE: Ensure this script uses ES5 only as the user may be running an old
-// version of Node, which this script wants to test against.
-//
-// This script will ensure that users are using a supported version of node
-// for the project.
+/* eslint-disable */
var exec = require('child_process').exec;
var existsSync = require('fs').existsSync;
diff --git a/internal/utils.js b/internal/utils.js
new file mode 100644
index 00000000..8a876dd4
--- /dev/null
+++ b/internal/utils.js
@@ -0,0 +1,41 @@
+import HappyPack from 'happypack';
+import notifier from 'node-notifier';
+import colors from 'colors/safe';
+import { execSync } from 'child_process';
+import appRootDir from 'app-root-dir';
+
+// Generates a HappyPack plugin.
+// @see https://github.com/amireh/happypack/
+export function happyPackPlugin({ name, loaders }) {
+ return new HappyPack({
+ id: name,
+ verbose: false,
+ threads: 4,
+ loaders,
+ });
+}
+
+export function log(options) {
+ const title = `${options.title.toUpperCase()}`;
+
+ if (options.notify) {
+ notifier.notify({
+ title,
+ message: options.message,
+ });
+ }
+
+ const level = options.level || 'info';
+ const msg = `==> ${title} -> ${options.message}`;
+
+ switch (level) {
+ case 'warn': console.log(colors.yellow(msg)); break;
+ case 'error': console.log(colors.bgRed.white(msg)); break;
+ case 'info':
+ default: console.log(colors.green(msg));
+ }
+}
+
+export function exec(command) {
+ execSync(command, { stdio: 'inherit', cwd: appRootDir.get() });
+}
diff --git a/internal/webpack/configFactory.js b/internal/webpack/configFactory.js
new file mode 100644
index 00000000..0e53a885
--- /dev/null
+++ b/internal/webpack/configFactory.js
@@ -0,0 +1,551 @@
+import appRootDir from 'app-root-dir';
+import AssetsPlugin from 'assets-webpack-plugin';
+import ExtractTextPlugin from 'extract-text-webpack-plugin';
+import nodeExternals from 'webpack-node-externals';
+import path from 'path';
+import webpack from 'webpack';
+import WebpackMd5Hash from 'webpack-md5-hash';
+
+import { happyPackPlugin } from '../utils';
+import { ifElse } from '../../shared/utils/logic';
+import { mergeDeep } from '../../shared/utils/objects';
+import { removeNil } from '../../shared/utils/arrays';
+import withServiceWorker from './withServiceWorker';
+import config from '../../config';
+
+/**
+ * Generates a webpack configuration for the target configuration.
+ *
+ * This function has been configured to support one "client/web" bundle, and any
+ * number of additional "node" bundles (e.g. our "server"). You can define
+ * additional node bundles by editing the project confuguration.
+ *
+ * @param {Object} buildOptions - The build options.
+ * @param {target} buildOptions.target - The bundle target (e.g 'clinet' || 'server').
+ * @param {target} buildOptions.optimize - Build an optimised version of the bundle?
+ *
+ * @return {Object} The webpack configuration.
+ */
+export default function webpackConfigFactory(buildOptions) {
+ const { target, optimize = false } = buildOptions;
+
+ const isProd = optimize;
+ const isDev = !isProd;
+ const isClient = target === 'client';
+ const isServer = target === 'server';
+ const isNode = !isClient;
+
+ // Preconfigure some ifElse helper instnaces. See the util docs for more
+ // information on how this util works.
+ const ifDev = ifElse(isDev);
+ const ifProd = ifElse(isProd);
+ const ifNode = ifElse(isNode);
+ const ifClient = ifElse(isClient);
+ const ifDevClient = ifElse(isDev && isClient);
+ const ifProdClient = ifElse(isProd && isClient);
+
+ console.log(
+ `==> Creating ${isProd
+ ? 'an optimised'
+ : 'a development'} bundle configuration for the "${target}"`,
+ );
+
+ const bundleConfig =
+ isServer || isClient
+ ? // This is either our "server" or "client" bundle.
+ config(['bundles', target])
+ : // Otherwise it must be an additional node bundle.
+ config(['additionalNodeBundles', target]);
+
+ if (!bundleConfig) {
+ throw new Error('No bundle configuration exists for target:', target);
+ }
+
+ let webpackConfig = {
+ // Define our entry chunks for our bundle.
+ entry: {
+ // We name our entry files "index" as it makes it easier for us to
+ // import bundle output files (e.g. `import server from './build/server';`)
+ index: removeNil([
+ // We are using polyfill.io instead of the very heavy babel-polyfill.
+ // Therefore we need to add the regenerator-runtime as polyfill.io
+ // doesn't support this.
+ ifClient('regenerator-runtime/runtime'),
+ // Extends hot reloading with the ability to hot path React Components.
+ // This should always be at the top of your entries list. Only put
+ // polyfills above it.
+ ifDevClient('react-hot-loader/patch'),
+ // Required to support hot reloading of our client.
+ ifDevClient(
+ () =>
+ `webpack-hot-middleware/client?reload=true&path=http://${config('host')}:${config(
+ 'clientDevServerPort',
+ )}/__webpack_hmr`,
+ ),
+ // The source entry file for the bundle.
+ path.resolve(appRootDir.get(), bundleConfig.srcEntryFile),
+ ]),
+ },
+
+ // Bundle output configuration.
+ output: {
+ // The dir in which our bundle should be output.
+ path: path.resolve(appRootDir.get(), bundleConfig.outputPath),
+ // The filename format for our bundle's entries.
+ filename: ifProdClient(
+ // For our production client bundles we include a hash in the filename.
+ // That way we won't hit any browser caching issues when our bundle
+ // output changes.
+ // Note: as we are using the WebpackMd5Hash plugin, the hashes will
+ // only change when the file contents change. This means we can
+ // set very aggressive caching strategies on our bundle output.
+ '[name]-[chunkhash].js',
+ // For any other bundle (typically a server/node) bundle we want a
+ // determinable output name to allow for easier importing/execution
+ // of the bundle by our scripts.
+ '[name].js',
+ ),
+ // The name format for any additional chunks produced for the bundle.
+ chunkFilename: '[name]-[chunkhash].js',
+ // When targetting node we will output our bundle as a commonjs2 module.
+ libraryTarget: ifNode('commonjs2', 'var'),
+ // This is the web path under which our webpack bundled client should
+ // be considered as being served from.
+ publicPath: ifDev(
+ // As we run a seperate development server for our client and server
+ // bundles we need to use an absolute http path for the public path.
+ `http://${config('host')}:${config('clientDevServerPort')}${config(
+ 'bundles.client.webPath',
+ )}`,
+ // Otherwise we expect our bundled client to be served from this path.
+ bundleConfig.webPath,
+ ),
+ },
+
+ target: isClient
+ ? // Only our client bundle will target the web as a runtime.
+ 'web'
+ : // Any other bundle must be targetting node as a runtime.
+ 'node',
+
+ // Ensure that webpack polyfills the following node features for use
+ // within any bundles that are targetting node as a runtime. This will be
+ // ignored otherwise.
+ node: {
+ __dirname: true,
+ __filename: true,
+ },
+
+ // Source map settings.
+ devtool: ifElse(
+ // Include source maps for ANY node bundle so that we can support
+ // nice stack traces for errors (the source maps get consumed by
+ // the `node-source-map-support` module to allow for this).
+ isNode ||
+ // Always include source maps for any development build.
+ isDev ||
+ // Allow for the following flag to force source maps even for production
+ // builds.
+ config('includeSourceMapsForOptimisedClientBundle'),
+ )(
+ // Produces an external source map (lives next to bundle output files).
+ 'source-map',
+ // Produces no source map.
+ 'hidden-source-map',
+ ),
+
+ // Performance budget feature.
+ // This enables checking of the output bundle size, which will result in
+ // warnings/errors if the bundle sizes are too large.
+ // We only want this enabled for our production client. Please
+ // see the webpack docs on how you can configure this to your own needs:
+ // https://webpack.js.org/configuration/performance/
+ performance: ifProdClient(
+ // Enable webpack's performance hints for production client builds.
+ { hints: 'warning' },
+ // Else we have to set a value of "false" if we don't want the feature.
+ false,
+ ),
+
+ resolve: {
+ // These extensions are tried when resolving a file.
+ extensions: config('bundleSrcTypes').map(ext => `.${ext}`),
+
+ // This is required for the modernizr-loader
+ // @see https://github.com/peerigon/modernizr-loader
+ alias: {
+ modernizr$: path.resolve(appRootDir.get(), './.modernizrrc'),
+ },
+ },
+
+ // We don't want our node_modules to be bundled with any bundle that is
+ // targetting the node environment, prefering them to be resolved via
+ // native node module system. Therefore we use the `webpack-node-externals`
+ // library to help us generate an externals configuration that will
+ // ignore all the node_modules.
+ externals: removeNil([
+ ifNode(() =>
+ nodeExternals(
+ // Some of our node_modules may contain files that depend on our
+ // webpack loaders, e.g. CSS or SASS.
+ // For these cases please make sure that the file extensions are
+ // registered within the following configuration setting.
+ {
+ whitelist: removeNil([
+ // We always want the source-map-support included in
+ // our node target bundles.
+ 'source-map-support/register',
+ ])
+ // And any items that have been whitelisted in the config need
+ // to be included in the bundling process too.
+ .concat(config('nodeExternalsFileTypeWhitelist') || []),
+ },
+ ),
+ ),
+ ]),
+
+ plugins: removeNil([
+ // This grants us source map support, which combined with our webpack
+ // source maps will give us nice stack traces for our node executed
+ // bundles.
+ // We use the BannerPlugin to make sure all of our chunks will get the
+ // source maps support installed.
+ ifNode(
+ () =>
+ new webpack.BannerPlugin({
+ banner: 'require("source-map-support").install();',
+ raw: true,
+ entryOnly: false,
+ }),
+ ),
+
+ // Implement webpack 3 scope hoisting that will remove function wrappers
+ // around your modules you may see some small size improvements. However,
+ // the significant improvement will be how fast the JavaScript loads in the browser.
+ ifProdClient(new webpack.optimize.ModuleConcatenationPlugin()),
+
+ // We use this so that our generated [chunkhash]'s are only different if
+ // the content for our respective chunks have changed. This optimises
+ // our long term browser caching strategy for our client bundle, avoiding
+ // cases where browsers end up having to download all the client chunks
+ // even though 1 or 2 may have only changed.
+ ifClient(() => new WebpackMd5Hash()),
+
+ // These are process.env flags that you can use in your code in order to
+ // have advanced control over what is included/excluded in your bundles.
+ // For example you may only want certain parts of your code to be
+ // included/ran under certain conditions.
+ //
+ // Any process.env.X values that are matched will be code substituted for
+ // the associated values below.
+ //
+ // For example you may have the following in your code:
+ // if (process.env.BUILD_FLAG_IS_CLIENT === 'true') {
+ // console.log('Foo');
+ // }
+ //
+ // If the BUILD_FLAG_IS_CLIENT was assigned a value of `false` the above
+ // code would be converted to the following by the webpack bundling
+ // process:
+ // if ('false' === 'true') {
+ // console.log('Foo');
+ // }
+ //
+ // When your bundle is built using the UglifyJsPlugin unreachable code
+ // blocks like in the example above will be removed from the bundle
+ // final output. This is helpful for extreme cases where you want to
+ // ensure that code is only included/executed on specific targets, or for
+ // doing debugging.
+ //
+ // NOTE: We are stringifying the values to keep them in line with the
+ // expected type of a typical process.env member (i.e. string).
+ // @see https://github.com/ctrlplusb/react-universally/issues/395
+ new webpack.EnvironmentPlugin({
+ // It is really important to use NODE_ENV=production in order to use
+ // optimised versions of some node_modules, such as React.
+ NODE_ENV: isProd ? 'production' : 'development',
+ // Is this the "client" bundle?
+ BUILD_FLAG_IS_CLIENT: JSON.stringify(isClient),
+ // Is this the "server" bundle?
+ BUILD_FLAG_IS_SERVER: JSON.stringify(isServer),
+ // Is this a node bundle?
+ BUILD_FLAG_IS_NODE: JSON.stringify(isNode),
+ // Is this a development build?
+ BUILD_FLAG_IS_DEV: JSON.stringify(isDev),
+ }),
+
+ // Generates a JSON file containing a map of all the output files for
+ // our webpack bundle. A necessisty for our server rendering process
+ // as we need to interogate these files in order to know what JS/CSS
+ // we need to inject into our HTML. We only need to know the assets for
+ // our client bundle.
+ ifClient(
+ () =>
+ new AssetsPlugin({
+ filename: config('bundleAssetsFileName'),
+ path: path.resolve(appRootDir.get(), bundleConfig.outputPath),
+ }),
+ ),
+
+ // We don't want webpack errors to occur during development as it will
+ // kill our dev servers.
+ ifDev(() => new webpack.NoEmitOnErrorsPlugin()),
+
+ // We need this plugin to enable hot reloading of our client.
+ ifDevClient(() => new webpack.HotModuleReplacementPlugin()),
+
+ // For our production client we need to make sure we pass the required
+ // configuration to ensure that the output is minimized/optimized.
+ ifProdClient(
+ () =>
+ new webpack.LoaderOptionsPlugin({
+ minimize: true,
+ }),
+ ),
+
+ // For our production client we need to make sure we pass the required
+ // configuration to ensure that the output is minimized/optimized.
+ ifProdClient(
+ () =>
+ new webpack.optimize.UglifyJsPlugin({
+ sourceMap: config('includeSourceMapsForOptimisedClientBundle'),
+ compress: {
+ screw_ie8: true,
+ warnings: false,
+ },
+ mangle: {
+ screw_ie8: true,
+ },
+ output: {
+ comments: false,
+ screw_ie8: true,
+ },
+ }),
+ ),
+
+ // For the production build of the client we need to extract the CSS into
+ // CSS files.
+ ifProdClient(
+ () =>
+ new ExtractTextPlugin({
+ filename: '[name]-[contenthash].css',
+ allChunks: true,
+ }),
+ ),
+
+ // -----------------------------------------------------------------------
+ // START: HAPPY PACK PLUGINS
+ //
+ // @see https://github.com/amireh/happypack/
+ //
+ // HappyPack allows us to use threads to execute our loaders. This means
+ // that we can get parallel execution of our loaders, significantly
+ // improving build and recompile times.
+ //
+ // This may not be an issue for you whilst your project is small, but
+ // the compile times can be signficant when the project scales. A lengthy
+ // compile time can significantly impare your development experience.
+ // Therefore we employ HappyPack to do threaded execution of our
+ // "heavy-weight" loaders.
+
+ // HappyPack 'javascript' instance.
+ happyPackPlugin({
+ name: 'happypack-javascript',
+ // We will use babel to do all our JS processing.
+ loaders: [
+ {
+ path: 'babel-loader',
+ // We will create a babel config and pass it through the plugin
+ // defined in the project configuration, allowing additional
+ // items to be added.
+ query: config('plugins.babelConfig')(
+ // Our "standard" babel config.
+ {
+ // We need to ensure that we do this otherwise the babelrc will
+ // get interpretted and for the current configuration this will mean
+ // that it will kill our webpack treeshaking feature as the modules
+ // transpilation has not been disabled within in.
+ babelrc: false,
+
+ presets: [
+ // JSX
+ 'react',
+ // Stage 3 javascript syntax.
+ // "Candidate: complete spec and initial browser implementations."
+ // Add anything lower than stage 3 at your own risk. :)
+ 'stage-3',
+ // For our client bundles we transpile all the latest ratified
+ // ES201X code into ES5, safe for browsers. We exclude module
+ // transilation as webpack takes care of this for us, doing
+ // tree shaking in the process.
+ ifClient(['env', { es2015: { modules: false } }]),
+ // For a node bundle we use the specific target against
+ // babel-preset-env so that only the unsupported features of
+ // our target node version gets transpiled.
+ ifNode(['env', { targets: { node: true } }]),
+ ].filter(x => x != null),
+
+ plugins: [
+ // Required to support react hot loader.
+ ifDevClient('react-hot-loader/babel'),
+ // This decorates our components with __self prop to JSX elements,
+ // which React will use to generate some runtime warnings.
+ ifDev('transform-react-jsx-self'),
+ // Adding this will give us the path to our components in the
+ // react dev tools.
+ ifDev('transform-react-jsx-source'),
+ // Replaces the React.createElement function with one that is
+ // more optimized for production.
+ // NOTE: Symbol needs to be polyfilled. Ensure this feature
+ // is enabled in the polyfill.io configuration.
+ ifProd('transform-react-inline-elements'),
+ // Hoists element creation to the top level for subtrees that
+ // are fully static, which reduces call to React.createElement
+ // and the resulting allocations. More importantly, it tells
+ // React that the subtree hasnโt changed so React can completely
+ // skip it when reconciling.
+ ifProd('transform-react-constant-elements'),
+ ].filter(x => x != null),
+ },
+ buildOptions,
+ ),
+ },
+ ],
+ }),
+
+ // HappyPack 'css' instance for development client.
+ ifDevClient(() =>
+ happyPackPlugin({
+ name: 'happypack-devclient-css',
+ loaders: [
+ 'style-loader',
+ {
+ path: 'css-loader',
+ // Include sourcemaps for dev experience++.
+ query: { sourceMap: true },
+ },
+ ],
+ }),
+ ),
+
+ // END: HAPPY PACK PLUGINS
+ // -----------------------------------------------------------------------
+ ]),
+ module: {
+ // Use strict export presence so that a missing export becomes a compile error.
+ strictExportPresence: true,
+ rules: [
+ {
+ // "oneOf" will traverse all imports with following loaders until one will
+ // match the requirements. When no loader matches it will fallback to the
+ // "file" loader at the end of the loader list.
+ oneOf: removeNil([
+ // JAVASCRIPT
+ {
+ test: /\.jsx?$/,
+ // We will defer all our js processing to the happypack plugin
+ // named "happypack-javascript".
+ // See the respective plugin within the plugins section for full
+ // details on what loader is being implemented.
+ loader: 'happypack/loader?id=happypack-javascript',
+ include: removeNil([
+ ...bundleConfig.srcPaths.map(srcPath => path.resolve(appRootDir.get(), srcPath)),
+ ifProdClient(path.resolve(appRootDir.get(), 'src/html')),
+ ]),
+ },
+
+ // CSS
+ // This is bound to our server/client bundles as we only expect to be
+ // serving the client bundle as a Single Page Application through the
+ // server.
+ ifElse(isClient || isServer)(
+ mergeDeep(
+ {
+ test: /\.css$/,
+ },
+ // For development clients we will defer all our css processing to the
+ // happypack plugin named "happypack-devclient-css".
+ // See the respective plugin within the plugins section for full
+ // details on what loader is being implemented.
+ ifDevClient({
+ loaders: ['happypack/loader?id=happypack-devclient-css'],
+ }),
+ // For a production client build we use the ExtractTextPlugin which
+ // will extract our CSS into CSS files. We don't use happypack here
+ // as there are some edge cases where it fails when used within
+ // an ExtractTextPlugin instance.
+ // Note: The ExtractTextPlugin needs to be registered within the
+ // plugins section too.
+ ifProdClient(() => ({
+ loader: ExtractTextPlugin.extract({
+ fallback: 'style-loader',
+ use: ['css-loader'],
+ }),
+ })),
+ // When targetting the server we use the "/locals" version of the
+ // css loader, as we don't need any css files for the server.
+ ifNode({
+ loaders: ['css-loader/locals'],
+ }),
+ ),
+ ),
+ {
+ test: /\.(graphql|gql)$/,
+ exclude: /node_modules/,
+ loader: 'graphql-tag/loader',
+ },
+ // MODERNIZR
+ // This allows you to do feature detection.
+ // @see https://modernizr.com/docs
+ // @see https://github.com/peerigon/modernizr-loader
+ ifClient({
+ test: /\.modernizrrc.js$/,
+ loader: 'modernizr-loader',
+ }),
+ ifClient({
+ test: /\.modernizrrc(\.json)?$/,
+ loader: 'modernizr-loader!json-loader',
+ }),
+
+ // ASSETS (Images/Fonts/etc)
+ // This is bound to our server/client bundles as we only expect to be
+ // serving the client bundle as a Single Page Application through the
+ // server.
+ ifElse(isClient || isServer)(() => ({
+ loader: 'file-loader',
+ exclude: [/\.js$/, /\.html$/, /\.json$/],
+ query: {
+ // What is the web path that the client bundle will be served from?
+ // The same value has to be used for both the client and the
+ // server bundles in order to ensure that SSR paths match the
+ // paths used on the client.
+ publicPath: isDev
+ ? // When running in dev mode the client bundle runs on a
+ // seperate port so we need to put an absolute path here.
+ `http://${config('host')}:${config('clientDevServerPort')}${config(
+ 'bundles.client.webPath',
+ )}`
+ : // Otherwise we just use the configured web path for the client.
+ config('bundles.client.webPath'),
+ // We only emit files when building a web bundle, for the server
+ // bundle we only care about the file loader being able to create
+ // the correct asset URLs.
+ emitFile: isClient,
+ },
+ })),
+
+ // Do not add any loader after file loader (fallback loader)
+ // Make sure to add the new loader(s) before the "file" loader.
+ ]),
+ },
+ ],
+ },
+ };
+
+ if (isProd && isClient) {
+ webpackConfig = withServiceWorker(webpackConfig, bundleConfig);
+ }
+
+ // Apply the configuration middleware.
+ return config('plugins.webpackConfig')(webpackConfig, buildOptions);
+}
diff --git a/internal/webpack/withServiceWorker/index.js b/internal/webpack/withServiceWorker/index.js
new file mode 100644
index 00000000..ec80907b
--- /dev/null
+++ b/internal/webpack/withServiceWorker/index.js
@@ -0,0 +1,143 @@
+import { sync as globSync } from 'glob';
+import appRootDir from 'app-root-dir';
+import path from 'path';
+import HtmlWebpackPlugin from 'html-webpack-plugin';
+import OfflinePlugin from 'offline-plugin';
+
+import config from '../../../config';
+
+import ClientConfig from '../../../config/components/ClientConfig';
+
+export default function withServiceWorker(webpackConfig, bundleConfig) {
+ if (!config('serviceWorker.enabled')) {
+ return webpackConfig;
+ }
+
+ // Offline Page generation.
+ //
+ // We use the HtmlWebpackPlugin to produce an "offline" html page that
+ // can be used by our service worker (see the OfflinePlugin below) in
+ // order support offline rendering of our application.
+ // We will only create the service worker required page if enabled in
+ // config and if we are building the production version of client.
+ webpackConfig.plugins.push(
+ new HtmlWebpackPlugin({
+ filename: config('serviceWorker.offlinePageFileName'),
+ template: `babel-loader!${path.resolve(__dirname, './offlinePageTemplate.js')}`,
+ production: true,
+ minify: {
+ removeComments: true,
+ collapseWhitespace: true,
+ removeRedundantAttributes: true,
+ useShortDoctype: true,
+ removeNilAttributes: true,
+ removeStyleLinkTypeAttributes: true,
+ keepClosingSlash: true,
+ minifyJS: true,
+ minifyCSS: true,
+ minifyURLs: true,
+ },
+ inject: true,
+ // We pass our config and client config script compoent as it will
+ // be needed by the offline template.
+ custom: {
+ config,
+ ClientConfig,
+ },
+ }),
+ );
+
+ // We use the offline-plugin to generate the service worker. It also
+ // provides a runtime installation script which gets executed within
+ // the client.
+ // @see https://github.com/NekR/offline-plugin
+ //
+ // This plugin generates a service worker script which as configured below
+ // will precache all our generated client bundle assets as well as our
+ // static "public" folder assets.
+ //
+ // It has also been configured to make use of a HtmlWebpackPlugin
+ // generated "offline" page so that users can still used the application
+ // offline.
+ //
+ // Any time our static files or generated bundle files change the user's
+ // cache will be updated.
+ //
+ // We will only include the service worker if enabled in config.
+ webpackConfig.plugins.push(
+ new OfflinePlugin({
+ // Setting this value lets the plugin know where our generated client
+ // assets will be served from.
+ // e.g. /client/
+ publicPath: bundleConfig.webPath,
+ // When using the publicPath we need to disable the "relativePaths"
+ // feature of this plugin.
+ relativePaths: false,
+ // Our offline support will be done via a service worker.
+ // Read more on them here:
+ // http://bit.ly/2f8q7Td
+ ServiceWorker: {
+ // The name of the service worker script that will get generated.
+ output: config('serviceWorker.fileName'),
+ // Enable events so that we can register updates.
+ events: true,
+ // By default the service worker will be ouput and served from the
+ // publicPath setting above in the root config of the OfflinePlugin.
+ // This means that it would be served from /client/sw.js
+ // We do not want this! Service workers have to be served from the
+ // root of our application in order for them to work correctly.
+ // Therefore we override the publicPath here. The sw.js will still
+ // live in at the /build/client/sw.js output location therefore in
+ // our server configuration we need to make sure that any requests
+ // to /sw.js will serve the /build/client/sw.js file.
+ publicPath: `/${config('serviceWorker.fileName')}`,
+ // When the user is offline then this html page will be used at
+ // the base that loads all our cached client scripts. This page
+ // is generated by the HtmlWebpackPlugin above, which takes care
+ // of injecting all of our client scripts into the body.
+ // Please see the HtmlWebpackPlugin configuration above for more
+ // information on this page.
+ navigateFallbackURL: `${bundleConfig.webPath}${config('serviceWorker.offlinePageFileName')}`,
+ },
+ // According to the Mozilla docs, AppCache is considered deprecated.
+ // @see https://mzl.la/1pOZ5wF
+ // It does however have much wider support compared to the newer
+ // Service Worker specification, so you could consider enabling it
+ // if you needed.
+ AppCache: false,
+ // Which external files should be included with the service worker?
+ // Add the polyfill io script as an external if it is enabled.
+ externals: (config('polyfillIO.enabled')
+ ? [`${config('polyfillIO.url')}?features=${config('polyfillIO.features').join(',')}`]
+ : [])
+ // Add any included public folder assets.
+ .concat(
+ config('serviceWorker.includePublicAssets').reduce((acc, cur) => {
+ const publicAssetPathGlob = path.resolve(
+ appRootDir.get(),
+ config('publicAssetsPath'),
+ cur,
+ );
+ const publicFileWebPaths = acc.concat(
+ // First get all the matching public folder files.
+ globSync(publicAssetPathGlob, { nodir: true })
+ // Then map them to relative paths against the public folder.
+ // We need to do this as we need the "web" paths for each one.
+ .map(publicFile =>
+ path.relative(
+ path.resolve(appRootDir.get(), config('publicAssetsPath')),
+ publicFile,
+ ),
+ )
+ // Add the leading "/" indicating the file is being hosted
+ // off the root of the application.
+ .map(relativePath => `/${relativePath}`),
+ );
+ return publicFileWebPaths;
+ }, []),
+ ),
+ }),
+ );
+
+ return webpackConfig;
+}
diff --git a/internal/webpack/withServiceWorker/offlinePageTemplate.js b/internal/webpack/withServiceWorker/offlinePageTemplate.js
new file mode 100644
index 00000000..f01d73ce
--- /dev/null
+++ b/internal/webpack/withServiceWorker/offlinePageTemplate.js
@@ -0,0 +1,20 @@
+/**
+ * This is used by the HtmlWebpackPlugin to generate an html page that we will
+ * use as a fallback for our service worker when the user is offline. It will
+ * embed all the required asset paths needed to bootstrap the application
+ * in an offline session.
+ */
+
+import React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+
+import HTML from '../../../shared/components/HTML';
+
+module.exports = function generate(context) {
+ // const config = context.htmlWebpackPlugin.options.custom.config;
+ const ClientConfig = context.htmlWebpackPlugin.options.custom.ClientConfig;
+ const html = renderToStaticMarkup(
+ } />,
+ );
+ return `${html}`;
+};
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..17f8779e
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,14 @@
+{
+ "name": "react-universally",
+ "version": "13.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "semver": {
+ "version": "5.4.1",
+ "resolved": "https://npm.strues.io/semver/-/semver-5.4.1.tgz",
+ "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
index 5edcae47..fe7ba2db 100644
--- a/package.json
+++ b/package.json
@@ -1,23 +1,44 @@
{
"name": "react-universally",
- "version": "12.0.0",
- "description": "A starter kit giving you the minimum requirements for a modern universal react application.",
+ "version": "13.0.0",
+ "description": "A starter kit for universal react applications.",
"main": "build/server/index.js",
"engines": {
"node": ">=6"
},
"scripts": {
- "preinstall": "node tools/scripts/preinstall",
- "clean": "babel-node tools/scripts/clean",
- "development": "babel-node tools/development",
- "build": "cross-env NODE_ENV=production babel-node tools/scripts/build",
- "analyze": "babel-node tools/scripts/analyze",
+ "analyze:client": "babel-node internal/scripts/analyze --client",
+ "analyze:server": "babel-node internal/scripts/analyze --server",
+ "build": "babel-node internal/scripts/build --optimize",
+ "build:dev": "babel-node internal/scripts/build",
+ "clean": "cross-env babel-node internal/scripts/clean",
+ "deploy": "babel-node internal/scripts/deploy",
+ "develop": "cross-env DEPLOYMENT=development babel-node internal/development",
+ "lint": "eslint client server shared config internal",
+ "precommit": "lint-staged",
+ "preinstall": "node internal/scripts/preinstall",
+ "prepush": "jest",
"start": "cross-env NODE_ENV=production node build/server",
- "deploy": "babel-node tools/scripts/deploy",
- "lint": "eslint src",
"test": "jest",
"test:coverage": "jest --coverage"
},
+ "lint-staged": {
+ "*.js": [
+ "prettier-eslint --write",
+ "git add"
+ ]
+ },
+ "jest": {
+ "collectCoverageFrom": [
+ "src/**/*.{js,jsx}"
+ ],
+ "snapshotSerializers": [
+ "/node_modules/enzyme-to-json/serializer"
+ ],
+ "testPathIgnorePatterns": [
+ "/(build|internal|node_modules|public)/"
+ ]
+ },
"repository": {
"type": "git",
"url": "git+https://github.com/ctrlplusb/react-universally.git"
@@ -31,101 +52,97 @@
"express",
"webpack"
],
- "contributors": [
- "Alin Porumb",
- "Benjamin Kniffler ",
- "Carson Perrotti ",
- "Christian Glombek ",
- "Christoph Werner",
- "David Edmondson",
- "Evgeny Boxer",
- "Joe Kohlmann ",
- "Lucian Lature ",
- "Steven Enten ",
- "Sean Matheson ",
- "Steven Truesdell "
- ],
"license": "MIT",
"bugs": {
"url": "https://github.com/ctrlplusb/react-universally/issues"
},
"homepage": "https://github.com/ctrlplusb/react-universally#readme",
- "jest": {
- "collectCoverageFrom": [
- "src/**/*.{js,jsx}"
- ],
- "snapshotSerializers": [
- "/node_modules/enzyme-to-json/serializer"
- ],
- "testPathIgnorePatterns": [
- "/(build|tools|node_modules|flow-typed|public)/"
- ]
- },
"dependencies": {
+ "apollo-client": "1.9.1",
"app-root-dir": "1.0.2",
- "code-split-component": "2.0.0-alpha.5",
+ "axios": "0.16.2",
+ "body-parser": "1.17.2",
"colors": "1.1.2",
- "compression": "1.6.2",
+ "compression": "1.7.0",
+ "cross-env": "5.0.5",
"dotenv": "4.0.0",
- "express": "4.14.0",
- "helmet": "3.3.0",
- "hpp": "0.2.1",
- "normalize.css": "5.0.0",
- "offline-plugin": "4.5.4",
- "react": "15.4.2",
- "react-dom": "15.4.2",
- "react-helmet": "3.3.0",
- "react-router": "4.0.0-alpha.6",
- "serialize-javascript": "1.3.0",
- "user-home": "2.0.0",
- "uuid": "3.0.1"
+ "express": "4.15.4",
+ "graphql": "0.10.5",
+ "graphql-server-express": "1.1.0",
+ "graphql-tag": "2.4.2",
+ "graphql-tools": "1.1.0",
+ "helmet": "3.8.1",
+ "hpp": "0.2.2",
+ "isomorphic-fetch": "2.2.1",
+ "lodash.merge": "4.6.0",
+ "modernizr": "3.5.0",
+ "normalize.css": "7.0.0",
+ "offline-plugin": "4.8.3",
+ "prop-types": "15.5.10",
+ "react": "15.6.1",
+ "react-apollo": "1.4.12",
+ "react-async-bootstrapper": "1.1.1",
+ "react-async-component": "1.0.0-beta.3",
+ "react-dom": "15.6.1",
+ "react-helmet": "5.1.3",
+ "react-redux": "5.0.6",
+ "react-router-dom": "4.1.2",
+ "redux": "3.7.2",
+ "redux-thunk": "2.2.0",
+ "serialize-javascript": "1.4.0",
+ "uuid": "3.1.0"
},
"devDependencies": {
- "assets-webpack-plugin": "3.5.0",
- "babel-cli": "6.18.0",
- "babel-core": "6.21.0",
- "babel-eslint": "7.1.1",
- "babel-jest": "18.0.0",
- "babel-loader": "6.2.10",
- "babel-plugin-transform-react-jsx-self": "6.11.0",
- "babel-plugin-transform-react-jsx-source": "6.9.0",
- "babel-polyfill": "6.20.0",
- "babel-preset-env": "1.1.7",
- "babel-preset-latest": "6.16.0",
- "babel-preset-react": "6.16.0",
- "babel-preset-stage-3": "6.17.0",
- "babel-template": "6.16.0",
- "chokidar": "1.6.1",
- "cross-env": "3.1.4",
- "css-loader": "0.26.1",
- "enzyme": "2.7.0",
- "enzyme-to-json": "1.4.5",
- "eslint": "3.13.0",
- "eslint-config-airbnb": "14.0.0",
- "eslint-plugin-flowtype": "2.29.2",
- "eslint-plugin-import": "2.2.0",
- "eslint-plugin-jsx-a11y": "3.0.2",
- "eslint-plugin-react": "6.9.0",
- "extract-text-webpack-plugin": "2.0.0-beta.4",
- "file-loader": "0.9.0",
- "glob": "7.1.1",
- "happypack": "3.0.2",
- "html-webpack-plugin": "2.26.0",
- "jest": "18.1.0",
+ "assets-webpack-plugin": "3.5.1",
+ "babel-cli": "6.24.1",
+ "babel-core": "6.25.0",
+ "babel-eslint": "7.2.3",
+ "babel-jest": "20.0.3",
+ "babel-loader": "7.1.1",
+ "babel-plugin-transform-react-constant-elements": "6.23.0",
+ "babel-plugin-transform-react-inline-elements": "6.22.0",
+ "babel-plugin-transform-react-jsx-self": "6.22.0",
+ "babel-plugin-transform-react-jsx-source": "6.22.0",
+ "babel-polyfill": "6.23.0",
+ "babel-preset-env": "1.6.0",
+ "babel-preset-react": "6.24.1",
+ "babel-preset-stage-3": "6.24.1",
+ "babel-template": "6.25.0",
+ "chokidar": "1.7.0",
+ "css-loader": "0.28.4",
+ "enzyme": "2.9.1",
+ "enzyme-to-json": "1.5.1",
+ "eslint": "4.4.1",
+ "eslint-config-airbnb": "15.1.0",
+ "eslint-plugin-import": "2.7.0",
+ "eslint-plugin-jsx-a11y": "6.0.2",
+ "eslint-plugin-react": "7.2.0",
+ "extract-text-webpack-plugin": "3.0.0",
+ "file-loader": "0.11.2",
+ "glob": "7.1.2",
+ "happypack": "4.0.0-beta.2",
+ "html-webpack-plugin": "2.30.1",
+ "husky": "0.14.3",
+ "jest": "20.0.4",
+ "lint-staged": "4.0.3",
"md5": "2.2.1",
- "node-notifier": "4.6.1",
- "react-addons-test-utils": "15.4.2",
- "react-hot-loader": "3.0.0-beta.6",
- "regenerator-runtime": "0.10.1",
- "rimraf": "2.5.4",
- "semver": "5.3.0",
- "source-map-support": "0.4.8",
- "style-loader": "0.13.1",
- "webpack": "2.2.0-rc.3",
- "webpack-bundle-analyzer": "2.2.1",
- "webpack-dev-middleware": "1.9.0",
- "webpack-hot-middleware": "2.15.0",
+ "modernizr-loader": "1.0.1",
+ "node-notifier": "5.1.2",
+ "prettier": "1.5.3",
+ "prettier-eslint": "6.4.2",
+ "prettier-eslint-cli": "4.1.1",
+ "react-hot-loader": "3.0.0-beta.7",
+ "react-test-renderer": "15.6.1",
+ "regenerator-runtime": "0.10.5",
+ "rimraf": "2.6.1",
+ "semver": "^5.4.1",
+ "source-map-support": "0.4.15",
+ "style-loader": "0.18.2",
+ "webpack": "3.5.3",
+ "webpack-bundle-analyzer": "2.9.0",
+ "webpack-dev-middleware": "1.12.0",
+ "webpack-hot-middleware": "2.18.2",
"webpack-md5-hash": "0.0.5",
- "webpack-node-externals": "1.5.4"
+ "webpack-node-externals": "1.6.0"
}
}
diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png
deleted file mode 100644
index c380747f..00000000
Binary files a/public/favicon-16x16.png and /dev/null differ
diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png
deleted file mode 100644
index bbe193ee..00000000
Binary files a/public/favicon-32x32.png and /dev/null differ
diff --git a/public/favicon.ico b/public/favicon.ico
index 9bfbca7b..a2f9dad1 100644
Binary files a/public/favicon.ico and b/public/favicon.ico differ
diff --git a/public/android-chrome-192x192.png b/public/favicons/android-chrome-192x192.png
similarity index 100%
rename from public/android-chrome-192x192.png
rename to public/favicons/android-chrome-192x192.png
diff --git a/public/android-chrome-512x512.png b/public/favicons/android-chrome-512x512.png
similarity index 100%
rename from public/android-chrome-512x512.png
rename to public/favicons/android-chrome-512x512.png
diff --git a/public/favicons/apple-touch-icon-114x114.png b/public/favicons/apple-touch-icon-114x114.png
new file mode 100644
index 00000000..18e13042
Binary files /dev/null and b/public/favicons/apple-touch-icon-114x114.png differ
diff --git a/public/favicons/apple-touch-icon-120x120.png b/public/favicons/apple-touch-icon-120x120.png
new file mode 100644
index 00000000..160dad8b
Binary files /dev/null and b/public/favicons/apple-touch-icon-120x120.png differ
diff --git a/public/favicons/apple-touch-icon-144x144.png b/public/favicons/apple-touch-icon-144x144.png
new file mode 100644
index 00000000..5d17a13e
Binary files /dev/null and b/public/favicons/apple-touch-icon-144x144.png differ
diff --git a/public/favicons/apple-touch-icon-152x152.png b/public/favicons/apple-touch-icon-152x152.png
new file mode 100644
index 00000000..1705b3da
Binary files /dev/null and b/public/favicons/apple-touch-icon-152x152.png differ
diff --git a/public/apple-touch-icon.png b/public/favicons/apple-touch-icon-180x180.png
similarity index 100%
rename from public/apple-touch-icon.png
rename to public/favicons/apple-touch-icon-180x180.png
diff --git a/public/favicons/apple-touch-icon-57x57.png b/public/favicons/apple-touch-icon-57x57.png
new file mode 100644
index 00000000..384b2ad7
Binary files /dev/null and b/public/favicons/apple-touch-icon-57x57.png differ
diff --git a/public/favicons/apple-touch-icon-60x60.png b/public/favicons/apple-touch-icon-60x60.png
new file mode 100644
index 00000000..dba185d1
Binary files /dev/null and b/public/favicons/apple-touch-icon-60x60.png differ
diff --git a/public/favicons/apple-touch-icon-72x72.png b/public/favicons/apple-touch-icon-72x72.png
new file mode 100644
index 00000000..9038eec9
Binary files /dev/null and b/public/favicons/apple-touch-icon-72x72.png differ
diff --git a/public/favicons/apple-touch-icon-76x76.png b/public/favicons/apple-touch-icon-76x76.png
new file mode 100644
index 00000000..6fcdf595
Binary files /dev/null and b/public/favicons/apple-touch-icon-76x76.png differ
diff --git a/public/favicons/favicon-128.png b/public/favicons/favicon-128.png
new file mode 100644
index 00000000..7dd19a99
Binary files /dev/null and b/public/favicons/favicon-128.png differ
diff --git a/public/favicons/favicon-16x16.png b/public/favicons/favicon-16x16.png
new file mode 100644
index 00000000..5d11d3b1
Binary files /dev/null and b/public/favicons/favicon-16x16.png differ
diff --git a/public/favicons/favicon-196x196.png b/public/favicons/favicon-196x196.png
new file mode 100644
index 00000000..802cb367
Binary files /dev/null and b/public/favicons/favicon-196x196.png differ
diff --git a/public/favicons/favicon-32x32.png b/public/favicons/favicon-32x32.png
new file mode 100644
index 00000000..457da1e9
Binary files /dev/null and b/public/favicons/favicon-32x32.png differ
diff --git a/public/favicons/favicon-96x96.png b/public/favicons/favicon-96x96.png
new file mode 100644
index 00000000..2d06113b
Binary files /dev/null and b/public/favicons/favicon-96x96.png differ
diff --git a/public/favicons/mstile-144x144.png b/public/favicons/mstile-144x144.png
new file mode 100644
index 00000000..5d17a13e
Binary files /dev/null and b/public/favicons/mstile-144x144.png differ
diff --git a/public/favicons/mstile-150x150.png b/public/favicons/mstile-150x150.png
new file mode 100644
index 00000000..084a84a0
Binary files /dev/null and b/public/favicons/mstile-150x150.png differ
diff --git a/public/favicons/mstile-310x150.png b/public/favicons/mstile-310x150.png
new file mode 100644
index 00000000..f26f8aa3
Binary files /dev/null and b/public/favicons/mstile-310x150.png differ
diff --git a/public/favicons/mstile-310x310.png b/public/favicons/mstile-310x310.png
new file mode 100644
index 00000000..a0393ce7
Binary files /dev/null and b/public/favicons/mstile-310x310.png differ
diff --git a/public/favicons/mstile-70x70.png b/public/favicons/mstile-70x70.png
new file mode 100644
index 00000000..7dd19a99
Binary files /dev/null and b/public/favicons/mstile-70x70.png differ
diff --git a/public/safari-pinned-tab.svg b/public/favicons/safari-pinned-tab.svg
similarity index 100%
rename from public/safari-pinned-tab.svg
rename to public/favicons/safari-pinned-tab.svg
diff --git a/public/mstile-150x150.png b/public/mstile-150x150.png
deleted file mode 100644
index f8ab8a3d..00000000
Binary files a/public/mstile-150x150.png and /dev/null differ
diff --git a/server/data/executableSchema.js b/server/data/executableSchema.js
new file mode 100644
index 00000000..cb09012b
--- /dev/null
+++ b/server/data/executableSchema.js
@@ -0,0 +1,10 @@
+import { makeExecutableSchema } from 'graphql-tools';
+import schema from './schema';
+import resolvers from './resolvers';
+
+const executableSchema = makeExecutableSchema({
+ typeDefs: schema,
+ resolvers,
+});
+
+export default executableSchema;
diff --git a/server/data/mockData.js b/server/data/mockData.js
new file mode 100644
index 00000000..f5547034
--- /dev/null
+++ b/server/data/mockData.js
@@ -0,0 +1,703 @@
+const posts = [
+ {
+ userId: 1,
+ id: 1,
+ title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
+ body:
+ 'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto',
+ },
+ {
+ userId: 1,
+ id: 2,
+ title: 'qui est esse',
+ body:
+ 'est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla',
+ },
+ {
+ userId: 1,
+ id: 3,
+ title: 'ea molestias quasi exercitationem repellat qui ipsa sit aut',
+ body:
+ 'et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut',
+ },
+ {
+ userId: 1,
+ id: 4,
+ title: 'eum et est occaecati',
+ body:
+ 'ullam et saepe reiciendis voluptatem adipisci\nsit amet autem assumenda provident rerum culpa\nquis hic commodi nesciunt rem tenetur doloremque ipsam iure\nquis sunt voluptatem rerum illo velit',
+ },
+ {
+ userId: 1,
+ id: 5,
+ title: 'nesciunt quas odio',
+ body:
+ 'repudiandae veniam quaerat sunt sed\nalias aut fugiat sit autem sed est\nvoluptatem omnis possimus esse voluptatibus quis\nest aut tenetur dolor neque',
+ },
+ {
+ userId: 1,
+ id: 6,
+ title: 'dolorem eum magni eos aperiam quia',
+ body:
+ 'ut aspernatur corporis harum nihil quis provident sequi\nmollitia nobis aliquid molestiae\nperspiciatis et ea nemo ab reprehenderit accusantium quas\nvoluptate dolores velit et doloremque molestiae',
+ },
+ {
+ userId: 1,
+ id: 7,
+ title: 'magnam facilis autem',
+ body:
+ 'dolore placeat quibusdam ea quo vitae\nmagni quis enim qui quis quo nemo aut saepe\nquidem repellat excepturi ut quia\nsunt ut sequi eos ea sed quas',
+ },
+ {
+ userId: 1,
+ id: 8,
+ title: 'dolorem dolore est ipsam',
+ body:
+ 'dignissimos aperiam dolorem qui eum\nfacilis quibusdam animi sint suscipit qui sint possimus cum\nquaerat magni maiores excepturi\nipsam ut commodi dolor voluptatum modi aut vitae',
+ },
+ {
+ userId: 1,
+ id: 9,
+ title: 'nesciunt iure omnis dolorem tempora et accusantium',
+ body:
+ 'consectetur animi nesciunt iure dolore\nenim quia ad\nveniam autem ut quam aut nobis\net est aut quod aut provident voluptas autem voluptas',
+ },
+ {
+ userId: 1,
+ id: 10,
+ title: 'optio molestias id quia eum',
+ body:
+ 'quo et expedita modi cum officia vel magni\ndoloribus qui repudiandae\nvero nisi sit\nquos veniam quod sed accusamus veritatis error',
+ },
+ {
+ userId: 2,
+ id: 11,
+ title: 'et ea vero quia laudantium autem',
+ body:
+ 'delectus reiciendis molestiae occaecati non minima eveniet qui voluptatibus\naccusamus in eum beatae sit\nvel qui neque voluptates ut commodi qui incidunt\nut animi commodi',
+ },
+ {
+ userId: 2,
+ id: 12,
+ title: 'in quibusdam tempore odit est dolorem',
+ body:
+ 'itaque id aut magnam\npraesentium quia et ea odit et ea voluptas et\nsapiente quia nihil amet occaecati quia id voluptatem\nincidunt ea est distinctio odio',
+ },
+ {
+ userId: 2,
+ id: 13,
+ title: 'dolorum ut in voluptas mollitia et saepe quo animi',
+ body:
+ 'aut dicta possimus sint mollitia voluptas commodi quo doloremque\niste corrupti reiciendis voluptatem eius rerum\nsit cumque quod eligendi laborum minima\nperferendis recusandae assumenda consectetur porro architecto ipsum ipsam',
+ },
+ {
+ userId: 2,
+ id: 14,
+ title: 'voluptatem eligendi optio',
+ body:
+ 'fuga et accusamus dolorum perferendis illo voluptas\nnon doloremque neque facere\nad qui dolorum molestiae beatae\nsed aut voluptas totam sit illum',
+ },
+ {
+ userId: 2,
+ id: 15,
+ title: 'eveniet quod temporibus',
+ body:
+ 'reprehenderit quos placeat\nvelit minima officia dolores impedit repudiandae molestiae nam\nvoluptas recusandae quis delectus\nofficiis harum fugiat vitae',
+ },
+ {
+ userId: 2,
+ id: 16,
+ title: 'sint suscipit perspiciatis velit dolorum rerum ipsa laboriosam odio',
+ body:
+ 'suscipit nam nisi quo aperiam aut\nasperiores eos fugit maiores voluptatibus quia\nvoluptatem quis ullam qui in alias quia est\nconsequatur magni mollitia accusamus ea nisi voluptate dicta',
+ },
+ {
+ userId: 2,
+ id: 17,
+ title: 'fugit voluptas sed molestias voluptatem provident',
+ body:
+ 'eos voluptas et aut odit natus earum\naspernatur fuga molestiae ullam\ndeserunt ratione qui eos\nqui nihil ratione nemo velit ut aut id quo',
+ },
+ {
+ userId: 2,
+ id: 18,
+ title: 'voluptate et itaque vero tempora molestiae',
+ body:
+ 'eveniet quo quis\nlaborum totam consequatur non dolor\nut et est repudiandae\nest voluptatem vel debitis et magnam',
+ },
+ {
+ userId: 2,
+ id: 19,
+ title: 'adipisci placeat illum aut reiciendis qui',
+ body:
+ 'illum quis cupiditate provident sit magnam\nea sed aut omnis\nveniam maiores ullam consequatur atque\nadipisci quo iste expedita sit quos voluptas',
+ },
+ {
+ userId: 2,
+ id: 20,
+ title: 'doloribus ad provident suscipit at',
+ body:
+ 'qui consequuntur ducimus possimus quisquam amet similique\nsuscipit porro ipsam amet\neos veritatis officiis exercitationem vel fugit aut necessitatibus totam\nomnis rerum consequatur expedita quidem cumque explicabo',
+ },
+ {
+ userId: 3,
+ id: 21,
+ title: 'asperiores ea ipsam voluptatibus modi minima quia sint',
+ body:
+ 'repellat aliquid praesentium dolorem quo\nsed totam minus non itaque\nnihil labore molestiae sunt dolor eveniet hic recusandae veniam\ntempora et tenetur expedita sunt',
+ },
+ {
+ userId: 3,
+ id: 22,
+ title: 'dolor sint quo a velit explicabo quia nam',
+ body:
+ 'eos qui et ipsum ipsam suscipit aut\nsed omnis non odio\nexpedita earum mollitia molestiae aut atque rem suscipit\nnam impedit esse',
+ },
+ {
+ userId: 3,
+ id: 23,
+ title: 'maxime id vitae nihil numquam',
+ body:
+ 'veritatis unde neque eligendi\nquae quod architecto quo neque vitae\nest illo sit tempora doloremque fugit quod\net et vel beatae sequi ullam sed tenetur perspiciatis',
+ },
+ {
+ userId: 3,
+ id: 24,
+ title: 'autem hic labore sunt dolores incidunt',
+ body:
+ 'enim et ex nulla\nomnis voluptas quia qui\nvoluptatem consequatur numquam aliquam sunt\ntotam recusandae id dignissimos aut sed asperiores deserunt',
+ },
+ {
+ userId: 3,
+ id: 25,
+ title: 'rem alias distinctio quo quis',
+ body:
+ 'ullam consequatur ut\nomnis quis sit vel consequuntur\nipsa eligendi ipsum molestiae et omnis error nostrum\nmolestiae illo tempore quia et distinctio',
+ },
+ {
+ userId: 3,
+ id: 26,
+ title: 'est et quae odit qui non',
+ body:
+ 'similique esse doloribus nihil accusamus\nomnis dolorem fuga consequuntur reprehenderit fugit recusandae temporibus\nperspiciatis cum ut laudantium\nomnis aut molestiae vel vero',
+ },
+ {
+ userId: 3,
+ id: 27,
+ title: 'quasi id et eos tenetur aut quo autem',
+ body:
+ 'eum sed dolores ipsam sint possimus debitis occaecati\ndebitis qui qui et\nut placeat enim earum aut odit facilis\nconsequatur suscipit necessitatibus rerum sed inventore temporibus consequatur',
+ },
+ {
+ userId: 3,
+ id: 28,
+ title: 'delectus ullam et corporis nulla voluptas sequi',
+ body:
+ 'non et quaerat ex quae ad maiores\nmaiores recusandae totam aut blanditiis mollitia quas illo\nut voluptatibus voluptatem\nsimilique nostrum eum',
+ },
+ {
+ userId: 3,
+ id: 29,
+ title: 'iusto eius quod necessitatibus culpa ea',
+ body:
+ 'odit magnam ut saepe sed non qui\ntempora atque nihil\naccusamus illum doloribus illo dolor\neligendi repudiandae odit magni similique sed cum maiores',
+ },
+ {
+ userId: 3,
+ id: 30,
+ title: 'a quo magni similique perferendis',
+ body:
+ 'alias dolor cumque\nimpedit blanditiis non eveniet odio maxime\nblanditiis amet eius quis tempora quia autem rem\na provident perspiciatis quia',
+ },
+ {
+ userId: 4,
+ id: 31,
+ title: 'ullam ut quidem id aut vel consequuntur',
+ body:
+ 'debitis eius sed quibusdam non quis consectetur vitae\nimpedit ut qui consequatur sed aut in\nquidem sit nostrum et maiores adipisci atque\nquaerat voluptatem adipisci repudiandae',
+ },
+ {
+ userId: 4,
+ id: 32,
+ title: 'doloremque illum aliquid sunt',
+ body:
+ 'deserunt eos nobis asperiores et hic\nest debitis repellat molestiae optio\nnihil ratione ut eos beatae quibusdam distinctio maiores\nearum voluptates et aut adipisci ea maiores voluptas maxime',
+ },
+ {
+ userId: 4,
+ id: 33,
+ title: 'qui explicabo molestiae dolorem',
+ body:
+ 'rerum ut et numquam laborum odit est sit\nid qui sint in\nquasi tenetur tempore aperiam et quaerat qui in\nrerum officiis sequi cumque quod',
+ },
+ {
+ userId: 4,
+ id: 34,
+ title: 'magnam ut rerum iure',
+ body:
+ 'ea velit perferendis earum ut voluptatem voluptate itaque iusto\ntotam pariatur in\nnemo voluptatem voluptatem autem magni tempora minima in\nest distinctio qui assumenda accusamus dignissimos officia nesciunt nobis',
+ },
+ {
+ userId: 4,
+ id: 35,
+ title: 'id nihil consequatur molestias animi provident',
+ body:
+ 'nisi error delectus possimus ut eligendi vitae\nplaceat eos harum cupiditate facilis reprehenderit voluptatem beatae\nmodi ducimus quo illum voluptas eligendi\net nobis quia fugit',
+ },
+ {
+ userId: 4,
+ id: 36,
+ title: 'fuga nam accusamus voluptas reiciendis itaque',
+ body:
+ 'ad mollitia et omnis minus architecto odit\nvoluptas doloremque maxime aut non ipsa qui alias veniam\nblanditiis culpa aut quia nihil cumque facere et occaecati\nqui aspernatur quia eaque ut aperiam inventore',
+ },
+ {
+ userId: 4,
+ id: 37,
+ title: 'provident vel ut sit ratione est',
+ body:
+ 'debitis et eaque non officia sed nesciunt pariatur vel\nvoluptatem iste vero et ea\nnumquam aut expedita ipsum nulla in\nvoluptates omnis consequatur aut enim officiis in quam qui',
+ },
+ {
+ userId: 4,
+ id: 38,
+ title: 'explicabo et eos deleniti nostrum ab id repellendus',
+ body:
+ 'animi esse sit aut sit nesciunt assumenda eum voluptas\nquia voluptatibus provident quia necessitatibus ea\nrerum repudiandae quia voluptatem delectus fugit aut id quia\nratione optio eos iusto veniam iure',
+ },
+ {
+ userId: 4,
+ id: 39,
+ title: 'eos dolorem iste accusantium est eaque quam',
+ body:
+ 'corporis rerum ducimus vel eum accusantium\nmaxime aspernatur a porro possimus iste omnis\nest in deleniti asperiores fuga aut\nvoluptas sapiente vel dolore minus voluptatem incidunt ex',
+ },
+ {
+ userId: 4,
+ id: 40,
+ title: 'enim quo cumque',
+ body:
+ 'ut voluptatum aliquid illo tenetur nemo sequi quo facilis\nipsum rem optio mollitia quas\nvoluptatem eum voluptas qui\nunde omnis voluptatem iure quasi maxime voluptas nam',
+ },
+ {
+ userId: 5,
+ id: 41,
+ title: 'non est facere',
+ body:
+ 'molestias id nostrum\nexcepturi molestiae dolore omnis repellendus quaerat saepe\nconsectetur iste quaerat tenetur asperiores accusamus ex ut\nnam quidem est ducimus sunt debitis saepe',
+ },
+ {
+ userId: 5,
+ id: 42,
+ title: 'commodi ullam sint et excepturi error explicabo praesentium voluptas',
+ body:
+ 'odio fugit voluptatum ducimus earum autem est incidunt voluptatem\nodit reiciendis aliquam sunt sequi nulla dolorem\nnon facere repellendus voluptates quia\nratione harum vitae ut',
+ },
+ {
+ userId: 5,
+ id: 43,
+ title: 'eligendi iste nostrum consequuntur adipisci praesentium sit beatae perferendis',
+ body:
+ 'similique fugit est\nillum et dolorum harum et voluptate eaque quidem\nexercitationem quos nam commodi possimus cum odio nihil nulla\ndolorum exercitationem magnam ex et a et distinctio debitis',
+ },
+ {
+ userId: 5,
+ id: 44,
+ title: 'optio dolor molestias sit',
+ body:
+ 'temporibus est consectetur dolore\net libero debitis vel velit laboriosam quia\nipsum quibusdam qui itaque fuga rem aut\nea et iure quam sed maxime ut distinctio quae',
+ },
+ {
+ userId: 5,
+ id: 45,
+ title: 'ut numquam possimus omnis eius suscipit laudantium iure',
+ body:
+ 'est natus reiciendis nihil possimus aut provident\nex et dolor\nrepellat pariatur est\nnobis rerum repellendus dolorem autem',
+ },
+ {
+ userId: 5,
+ id: 46,
+ title: 'aut quo modi neque nostrum ducimus',
+ body:
+ 'voluptatem quisquam iste\nvoluptatibus natus officiis facilis dolorem\nquis quas ipsam\nvel et voluptatum in aliquid',
+ },
+ {
+ userId: 5,
+ id: 47,
+ title: 'quibusdam cumque rem aut deserunt',
+ body:
+ 'voluptatem assumenda ut qui ut cupiditate aut impedit veniam\noccaecati nemo illum voluptatem laudantium\nmolestiae beatae rerum ea iure soluta nostrum\neligendi et voluptate',
+ },
+ {
+ userId: 5,
+ id: 48,
+ title: 'ut voluptatem illum ea doloribus itaque eos',
+ body:
+ 'voluptates quo voluptatem facilis iure occaecati\nvel assumenda rerum officia et\nillum perspiciatis ab deleniti\nlaudantium repellat ad ut et autem reprehenderit',
+ },
+ {
+ userId: 5,
+ id: 49,
+ title: 'laborum non sunt aut ut assumenda perspiciatis voluptas',
+ body:
+ 'inventore ab sint\nnatus fugit id nulla sequi architecto nihil quaerat\neos tenetur in in eum veritatis non\nquibusdam officiis aspernatur cumque aut commodi aut',
+ },
+ {
+ userId: 5,
+ id: 50,
+ title: 'repellendus qui recusandae incidunt voluptates tenetur qui omnis exercitationem',
+ body:
+ 'error suscipit maxime adipisci consequuntur recusandae\nvoluptas eligendi et est et voluptates\nquia distinctio ab amet quaerat molestiae et vitae\nadipisci impedit sequi nesciunt quis consectetur',
+ },
+ {
+ userId: 6,
+ id: 51,
+ title: 'soluta aliquam aperiam consequatur illo quis voluptas',
+ body:
+ 'sunt dolores aut doloribus\ndolore doloribus voluptates tempora et\ndoloremque et quo\ncum asperiores sit consectetur dolorem',
+ },
+ {
+ userId: 6,
+ id: 52,
+ title: 'qui enim et consequuntur quia animi quis voluptate quibusdam',
+ body:
+ 'iusto est quibusdam fuga quas quaerat molestias\na enim ut sit accusamus enim\ntemporibus iusto accusantium provident architecto\nsoluta esse reprehenderit qui laborum',
+ },
+ {
+ userId: 6,
+ id: 53,
+ title: 'ut quo aut ducimus alias',
+ body:
+ 'minima harum praesentium eum rerum illo dolore\nquasi exercitationem rerum nam\nporro quis neque quo\nconsequatur minus dolor quidem veritatis sunt non explicabo similique',
+ },
+ {
+ userId: 6,
+ id: 54,
+ title: 'sit asperiores ipsam eveniet odio non quia',
+ body:
+ 'totam corporis dignissimos\nvitae dolorem ut occaecati accusamus\nex velit deserunt\net exercitationem vero incidunt corrupti mollitia',
+ },
+ {
+ userId: 6,
+ id: 55,
+ title: 'sit vel voluptatem et non libero',
+ body:
+ 'debitis excepturi ea perferendis harum libero optio\neos accusamus cum fuga ut sapiente repudiandae\net ut incidunt omnis molestiae\nnihil ut eum odit',
+ },
+ {
+ userId: 6,
+ id: 56,
+ title: 'qui et at rerum necessitatibus',
+ body:
+ 'aut est omnis dolores\nneque rerum quod ea rerum velit pariatur beatae excepturi\net provident voluptas corrupti\ncorporis harum reprehenderit dolores eligendi',
+ },
+ {
+ userId: 6,
+ id: 57,
+ title: 'sed ab est est',
+ body:
+ 'at pariatur consequuntur earum quidem\nquo est laudantium soluta voluptatem\nqui ullam et est\net cum voluptas voluptatum repellat est',
+ },
+ {
+ userId: 6,
+ id: 58,
+ title: 'voluptatum itaque dolores nisi et quasi',
+ body:
+ 'veniam voluptatum quae adipisci id\net id quia eos ad et dolorem\naliquam quo nisi sunt eos impedit error\nad similique veniam',
+ },
+ {
+ userId: 6,
+ id: 59,
+ title: 'qui commodi dolor at maiores et quis id accusantium',
+ body:
+ 'perspiciatis et quam ea autem temporibus non voluptatibus qui\nbeatae a earum officia nesciunt dolores suscipit voluptas et\nanimi doloribus cum rerum quas et magni\net hic ut ut commodi expedita sunt',
+ },
+ {
+ userId: 6,
+ id: 60,
+ title: 'consequatur placeat omnis quisquam quia reprehenderit fugit veritatis facere',
+ body:
+ 'asperiores sunt ab assumenda cumque modi velit\nqui esse omnis\nvoluptate et fuga perferendis voluptas\nillo ratione amet aut et omnis',
+ },
+ {
+ userId: 7,
+ id: 61,
+ title: 'voluptatem doloribus consectetur est ut ducimus',
+ body:
+ 'ab nemo optio odio\ndelectus tenetur corporis similique nobis repellendus rerum omnis facilis\nvero blanditiis debitis in nesciunt doloribus dicta dolores\nmagnam minus velit',
+ },
+ {
+ userId: 7,
+ id: 62,
+ title: 'beatae enim quia vel',
+ body:
+ 'enim aspernatur illo distinctio quae praesentium\nbeatae alias amet delectus qui voluptate distinctio\nodit sint accusantium autem omnis\nquo molestiae omnis ea eveniet optio',
+ },
+ {
+ userId: 7,
+ id: 63,
+ title: 'voluptas blanditiis repellendus animi ducimus error sapiente et suscipit',
+ body:
+ 'enim adipisci aspernatur nemo\nnumquam omnis facere dolorem dolor ex quis temporibus incidunt\nab delectus culpa quo reprehenderit blanditiis asperiores\naccusantium ut quam in voluptatibus voluptas ipsam dicta',
+ },
+ {
+ userId: 7,
+ id: 64,
+ title: 'et fugit quas eum in in aperiam quod',
+ body:
+ 'id velit blanditiis\neum ea voluptatem\nmolestiae sint occaecati est eos perspiciatis\nincidunt a error provident eaque aut aut qui',
+ },
+ {
+ userId: 7,
+ id: 65,
+ title: 'consequatur id enim sunt et et',
+ body:
+ 'voluptatibus ex esse\nsint explicabo est aliquid cumque adipisci fuga repellat labore\nmolestiae corrupti ex saepe at asperiores et perferendis\nnatus id esse incidunt pariatur',
+ },
+ {
+ userId: 7,
+ id: 66,
+ title: 'repudiandae ea animi iusto',
+ body:
+ 'officia veritatis tenetur vero qui itaque\nsint non ratione\nsed et ut asperiores iusto eos molestiae nostrum\nveritatis quibusdam et nemo iusto saepe',
+ },
+ {
+ userId: 7,
+ id: 67,
+ title: 'aliquid eos sed fuga est maxime repellendus',
+ body:
+ 'reprehenderit id nostrum\nvoluptas doloremque pariatur sint et accusantium quia quod aspernatur\net fugiat amet\nnon sapiente et consequatur necessitatibus molestiae',
+ },
+ {
+ userId: 7,
+ id: 68,
+ title: 'odio quis facere architecto reiciendis optio',
+ body:
+ 'magnam molestiae perferendis quisquam\nqui cum reiciendis\nquaerat animi amet hic inventore\nea quia deleniti quidem saepe porro velit',
+ },
+ {
+ userId: 7,
+ id: 69,
+ title: 'fugiat quod pariatur odit minima',
+ body:
+ 'officiis error culpa consequatur modi asperiores et\ndolorum assumenda voluptas et vel qui aut vel rerum\nvoluptatum quisquam perspiciatis quia rerum consequatur totam quas\nsequi commodi repudiandae asperiores et saepe a',
+ },
+ {
+ userId: 7,
+ id: 70,
+ title: 'voluptatem laborum magni',
+ body:
+ 'sunt repellendus quae\nest asperiores aut deleniti esse accusamus repellendus quia aut\nquia dolorem unde\neum tempora esse dolore',
+ },
+ {
+ userId: 8,
+ id: 71,
+ title: 'et iusto veniam et illum aut fuga',
+ body:
+ 'occaecati a doloribus\niste saepe consectetur placeat eum voluptate dolorem et\nqui quo quia voluptas\nrerum ut id enim velit est perferendis',
+ },
+ {
+ userId: 8,
+ id: 72,
+ title: 'sint hic doloribus consequatur eos non id',
+ body:
+ 'quam occaecati qui deleniti consectetur\nconsequatur aut facere quas exercitationem aliquam hic voluptas\nneque id sunt ut aut accusamus\nsunt consectetur expedita inventore velit',
+ },
+ {
+ userId: 8,
+ id: 73,
+ title: 'consequuntur deleniti eos quia temporibus ab aliquid at',
+ body:
+ 'voluptatem cumque tenetur consequatur expedita ipsum nemo quia explicabo\naut eum minima consequatur\ntempore cumque quae est et\net in consequuntur voluptatem voluptates aut',
+ },
+ {
+ userId: 8,
+ id: 74,
+ title: 'enim unde ratione doloribus quas enim ut sit sapiente',
+ body:
+ 'odit qui et et necessitatibus sint veniam\nmollitia amet doloremque molestiae commodi similique magnam et quam\nblanditiis est itaque\nquo et tenetur ratione occaecati molestiae tempora',
+ },
+ {
+ userId: 8,
+ id: 75,
+ title: 'dignissimos eum dolor ut enim et delectus in',
+ body:
+ 'commodi non non omnis et voluptas sit\nautem aut nobis magnam et sapiente voluptatem\net laborum repellat qui delectus facilis temporibus\nrerum amet et nemo voluptate expedita adipisci error dolorem',
+ },
+ {
+ userId: 8,
+ id: 76,
+ title: 'doloremque officiis ad et non perferendis',
+ body:
+ 'ut animi facere\ntotam iusto tempore\nmolestiae eum aut et dolorem aperiam\nquaerat recusandae totam odio',
+ },
+ {
+ userId: 8,
+ id: 77,
+ title: 'necessitatibus quasi exercitationem odio',
+ body:
+ 'modi ut in nulla repudiandae dolorum nostrum eos\naut consequatur omnis\nut incidunt est omnis iste et quam\nvoluptates sapiente aliquam asperiores nobis amet corrupti repudiandae provident',
+ },
+ {
+ userId: 8,
+ id: 78,
+ title: 'quam voluptatibus rerum veritatis',
+ body:
+ 'nobis facilis odit tempore cupiditate quia\nassumenda doloribus rerum qui ea\nillum et qui totam\naut veniam repellendus',
+ },
+ {
+ userId: 8,
+ id: 79,
+ title: 'pariatur consequatur quia magnam autem omnis non amet',
+ body:
+ 'libero accusantium et et facere incidunt sit dolorem\nnon excepturi qui quia sed laudantium\nquisquam molestiae ducimus est\nofficiis esse molestiae iste et quos',
+ },
+ {
+ userId: 8,
+ id: 80,
+ title: 'labore in ex et explicabo corporis aut quas',
+ body:
+ 'ex quod dolorem ea eum iure qui provident amet\nquia qui facere excepturi et repudiandae\nasperiores molestias provident\nminus incidunt vero fugit rerum sint sunt excepturi provident',
+ },
+ {
+ userId: 9,
+ id: 81,
+ title: 'tempora rem veritatis voluptas quo dolores vero',
+ body:
+ 'facere qui nesciunt est voluptatum voluptatem nisi\nsequi eligendi necessitatibus ea at rerum itaque\nharum non ratione velit laboriosam quis consequuntur\nex officiis minima doloremque voluptas ut aut',
+ },
+ {
+ userId: 9,
+ id: 82,
+ title: 'laudantium voluptate suscipit sunt enim enim',
+ body:
+ 'ut libero sit aut totam inventore sunt\nporro sint qui sunt molestiae\nconsequatur cupiditate qui iste ducimus adipisci\ndolor enim assumenda soluta laboriosam amet iste delectus hic',
+ },
+ {
+ userId: 9,
+ id: 83,
+ title: 'odit et voluptates doloribus alias odio et',
+ body:
+ 'est molestiae facilis quis tempora numquam nihil qui\nvoluptate sapiente consequatur est qui\nnecessitatibus autem aut ipsa aperiam modi dolore numquam\nreprehenderit eius rem quibusdam',
+ },
+ {
+ userId: 9,
+ id: 84,
+ title: 'optio ipsam molestias necessitatibus occaecati facilis veritatis dolores aut',
+ body:
+ 'sint molestiae magni a et quos\neaque et quasi\nut rerum debitis similique veniam\nrecusandae dignissimos dolor incidunt consequatur odio',
+ },
+ {
+ userId: 9,
+ id: 85,
+ title: 'dolore veritatis porro provident adipisci blanditiis et sunt',
+ body:
+ 'similique sed nisi voluptas iusto omnis\nmollitia et quo\nassumenda suscipit officia magnam sint sed tempora\nenim provident pariatur praesentium atque animi amet ratione',
+ },
+ {
+ userId: 9,
+ id: 86,
+ title: 'placeat quia et porro iste',
+ body:
+ 'quasi excepturi consequatur iste autem temporibus sed molestiae beatae\net quaerat et esse ut\nvoluptatem occaecati et vel explicabo autem\nasperiores pariatur deserunt optio',
+ },
+ {
+ userId: 9,
+ id: 87,
+ title: 'nostrum quis quasi placeat',
+ body:
+ 'eos et molestiae\nnesciunt ut a\ndolores perspiciatis repellendus repellat aliquid\nmagnam sint rem ipsum est',
+ },
+ {
+ userId: 9,
+ id: 88,
+ title: 'sapiente omnis fugit eos',
+ body:
+ 'consequatur omnis est praesentium\nducimus non iste\nneque hic deserunt\nvoluptatibus veniam cum et rerum sed',
+ },
+ {
+ userId: 9,
+ id: 89,
+ title: 'sint soluta et vel magnam aut ut sed qui',
+ body:
+ 'repellat aut aperiam totam temporibus autem et\narchitecto magnam ut\nconsequatur qui cupiditate rerum quia soluta dignissimos nihil iure\ntempore quas est',
+ },
+ {
+ userId: 9,
+ id: 90,
+ title: 'ad iusto omnis odit dolor voluptatibus',
+ body:
+ 'minus omnis soluta quia\nqui sed adipisci voluptates illum ipsam voluptatem\neligendi officia ut in\neos soluta similique molestias praesentium blanditiis',
+ },
+ {
+ userId: 10,
+ id: 91,
+ title: 'aut amet sed',
+ body:
+ 'libero voluptate eveniet aperiam sed\nsunt placeat suscipit molestias\nsimilique fugit nam natus\nexpedita consequatur consequatur dolores quia eos et placeat',
+ },
+ {
+ userId: 10,
+ id: 92,
+ title: 'ratione ex tenetur perferendis',
+ body:
+ 'aut et excepturi dicta laudantium sint rerum nihil\nlaudantium et at\na neque minima officia et similique libero et\ncommodi voluptate qui',
+ },
+ {
+ userId: 10,
+ id: 93,
+ title: 'beatae soluta recusandae',
+ body:
+ 'dolorem quibusdam ducimus consequuntur dicta aut quo laboriosam\nvoluptatem quis enim recusandae ut sed sunt\nnostrum est odit totam\nsit error sed sunt eveniet provident qui nulla',
+ },
+ {
+ userId: 10,
+ id: 94,
+ title: 'qui qui voluptates illo iste minima',
+ body:
+ 'aspernatur expedita soluta quo ab ut similique\nexpedita dolores amet\nsed temporibus distinctio magnam saepe deleniti\nomnis facilis nam ipsum natus sint similique omnis',
+ },
+ {
+ userId: 10,
+ id: 95,
+ title: 'id minus libero illum nam ad officiis',
+ body:
+ 'earum voluptatem facere provident blanditiis velit laboriosam\npariatur accusamus odio saepe\ncumque dolor qui a dicta ab doloribus consequatur omnis\ncorporis cupiditate eaque assumenda ad nesciunt',
+ },
+ {
+ userId: 10,
+ id: 96,
+ title: 'quaerat velit veniam amet cupiditate aut numquam ut sequi',
+ body:
+ 'in non odio excepturi sint eum\nlabore voluptates vitae quia qui et\ninventore itaque rerum\nveniam non exercitationem delectus aut',
+ },
+ {
+ userId: 10,
+ id: 97,
+ title: 'quas fugiat ut perspiciatis vero provident',
+ body:
+ 'eum non blanditiis soluta porro quibusdam voluptas\nvel voluptatem qui placeat dolores qui velit aut\nvel inventore aut cumque culpa explicabo aliquid at\nperspiciatis est et voluptatem dignissimos dolor itaque sit nam',
+ },
+ {
+ userId: 10,
+ id: 98,
+ title: 'laboriosam dolor voluptates',
+ body:
+ 'doloremque ex facilis sit sint culpa\nsoluta assumenda eligendi non ut eius\nsequi ducimus vel quasi\nveritatis est dolores',
+ },
+ {
+ userId: 10,
+ id: 99,
+ title: 'temporibus sit alias delectus eligendi possimus magni',
+ body:
+ 'quo deleniti praesentium dicta non quod\naut est molestias\nmolestias et officia quis nihil\nitaque dolorem quia',
+ },
+ {
+ userId: 10,
+ id: 100,
+ title: 'at nam consequatur ea labore ea harum',
+ body:
+ 'cupiditate quo est a modi nesciunt soluta\nipsa voluptas error itaque dicta in\nautem qui minus magnam et distinctio eum\naccusamus ratione error aut',
+ },
+];
+export default posts;
diff --git a/server/data/resolvers.js b/server/data/resolvers.js
new file mode 100644
index 00000000..e7e4902a
--- /dev/null
+++ b/server/data/resolvers.js
@@ -0,0 +1,20 @@
+/* eslint-disable no-unused-vars */
+import posts from './mockData';
+
+const resolveFunctions = {
+ PostList: {
+ posts() {
+ return posts;
+ },
+ },
+ Query: {
+ postList() {
+ return true;
+ },
+ singlePost(_, args, ctx) {
+ return posts[args.id];
+ },
+ },
+};
+
+export default resolveFunctions;
diff --git a/server/data/schema.js b/server/data/schema.js
new file mode 100644
index 00000000..ff64ce57
--- /dev/null
+++ b/server/data/schema.js
@@ -0,0 +1,27 @@
+const schema = `
+ type Post {
+ # the post's id
+ id: Int!
+ # the title of the post
+ title: String!
+ # the id of the author
+ userId: Int
+ # the post content
+ body: String!
+ }
+ # 100 post objects from JSONplaceholder
+ type PostList {
+ posts: [Post]
+ }
+ # the schema allows the following query:
+ type Query {
+ postList: PostList
+ singlePost(id: Int!): Post
+ }
+
+ schema {
+ query: Query
+ }
+`;
+
+export default schema;
diff --git a/server/index.js b/server/index.js
new file mode 100644
index 00000000..ad2c41d7
--- /dev/null
+++ b/server/index.js
@@ -0,0 +1,94 @@
+/* eslint-disable no-console */
+import 'isomorphic-fetch/fetch-npm-node';
+import express from 'express';
+import bodyParser from 'body-parser';
+import compression from 'compression';
+import { resolve as pathResolve } from 'path';
+import appRootDir from 'app-root-dir';
+import { graphqlExpress, graphiqlExpress } from 'graphql-server-express';
+import reactApplication from './middleware/reactApplication';
+import security from './middleware/security';
+import clientBundle from './middleware/clientBundle';
+import serviceWorker from './middleware/serviceWorker';
+import offlinePage from './middleware/offlinePage';
+import errorHandlers from './middleware/errorHandlers';
+import executableSchema from './data/executableSchema';
+
+import config from '../config';
+
+// Create our express based server.
+const app = express();
+
+// Don't expose any software information to potential hackers.
+app.disable('x-powered-by');
+
+// Security middlewares.
+app.use(...security);
+
+// Gzip compress the responses.
+app.use(compression());
+
+const graphqlHandler = graphqlExpress((req) => {
+ const query = req.query.query || req.body.query;
+ if (query && query.length > 2000) {
+ // None of our app's queries are this long
+ // Probably indicates someone trying to send an overly expensive query
+ throw new Error('Query too large.');
+ }
+ return {
+ schema: executableSchema,
+ context: {
+ req,
+ },
+ debug: true,
+ pretty: process.env.NODE_ENV !== 'production',
+ formatError: error => ({
+ message: error.message,
+ state: error.originalError && error.originalError.state,
+ locations: error.locations,
+ path: error.path,
+ }),
+ };
+});
+
+if (process.env.NODE_ENV !== 'production') {
+ app.use(
+ '/graphiql',
+ graphiqlExpress({
+ endpointURL: '/graphql',
+ }),
+ );
+}
+app.use('/graphql', bodyParser.json(), graphqlHandler);
+// Register our service worker generated by our webpack config.
+// We do not want the service worker registered for development builds, and
+// additionally only want it registered if the config allows.
+if (process.env.BUILD_FLAG_IS_DEV === 'false' && config('serviceWorker.enabled')) {
+ app.get(`/${config('serviceWorker.fileName')}`, serviceWorker);
+ app.get(
+ `${config('bundles.client.webPath')}${config('serviceWorker.offlinePageFileName')}`,
+ offlinePage,
+ );
+}
+
+// Configure serving of our client bundle.
+app.use(config('bundles.client.webPath'), clientBundle);
+
+// Configure static serving of our "public" root http path static files.
+// Note: these will be served off the root (i.e. '/') of our application.
+app.use(express.static(pathResolve(appRootDir.get(), config('publicAssetsPath'))));
+
+// The React application middleware.
+app.get('*', reactApplication);
+
+// Error Handler middlewares.
+app.use(...errorHandlers);
+
+// Create an http listener for our express app.
+const listener = app.listen(config('port'), () =>
+ console.log(`Server listening on port ${config('port')}`),
+);
+
+// We export the listener as it will be handy for our development hot reloader,
+// or for exposing a general extension layer for application customisations.
+export default listener;
diff --git a/server/middleware/clientBundle.js b/server/middleware/clientBundle.js
new file mode 100644
index 00000000..c136bdb7
--- /dev/null
+++ b/server/middleware/clientBundle.js
@@ -0,0 +1,11 @@
+import express from 'express';
+import { resolve as pathResolve } from 'path';
+import appRootDir from 'app-root-dir';
+import config from '../../config';
+
+/**
+ * Middleware to server our client bundle.
+ */
+export default express.static(pathResolve(appRootDir.get(), config('bundles.client.outputPath')), {
+ maxAge: config('browserCacheMaxAge'),
+});
diff --git a/server/middleware/errorHandlers.js b/server/middleware/errorHandlers.js
new file mode 100644
index 00000000..c727cf43
--- /dev/null
+++ b/server/middleware/errorHandlers.js
@@ -0,0 +1,31 @@
+/* eslint-disable no-console */
+/* eslint-disable no-unused-vars */
+
+const errorHandlersMiddleware = [
+ /**
+ * 404 errors middleware.
+ *
+ * NOTE: the react application middleware hands 404 paths, but it is good to
+ * have this backup for paths not handled by the react middleware. For
+ * example you may bind a /api path to express.
+ */
+ function notFoundMiddlware(req, res, next) {
+ res.status(404).send('Sorry, that resource was not found.');
+ },
+
+ /**
+ * 500 errors middleware.
+ *
+ * NOTE: You must provide specify all 4 parameters on this callback function
+ * even if they aren't used, otherwise it won't be used.
+ */
+ function unexpectedErrorMiddleware(err, req, res, next) {
+ if (err) {
+ console.log(err);
+ console.log(err.stack);
+ }
+ res.status(500).send('Sorry, an unexpected error occurred.');
+ },
+];
+
+export default errorHandlersMiddleware;
diff --git a/src/server/middleware/offlinePage.js b/server/middleware/offlinePage.js
similarity index 56%
rename from src/server/middleware/offlinePage.js
rename to server/middleware/offlinePage.js
index a3f5d96c..c85bbdff 100644
--- a/src/server/middleware/offlinePage.js
+++ b/server/middleware/offlinePage.js
@@ -3,12 +3,12 @@
import { readFile } from 'fs';
import { resolve as pathResolve } from 'path';
import appRootDir from 'app-root-dir';
-import config from '../../../config';
+
+import config from '../../config';
/**
- * We need a middleware to intercept calls to our offline page to ensure that
- * inline scripts get the correct nonce value injected into them. Otherwise
- * we can't provide client config values to the offline page.
+ * Middleware to intercept calls to our offline page to ensure that
+ * inline scripts get a nonce value attached to them.
*/
export default function offlinePageMiddleware(req, res, next) {
// We should have had a nonce provided to us. See the server/index.js for
@@ -17,20 +17,26 @@ export default function offlinePageMiddleware(req, res, next) {
throw new Error('A "nonce" value has not been attached to the response');
}
const nonce = res.locals.nonce;
+
readFile(
+ // Path to the offline page.
pathResolve(
appRootDir.get(),
- config.bundles.client.outputPath,
- config.serviceWorker.offlinePageFileName,
+ config('bundles.client.outputPath'),
+ config('serviceWorker.offlinePageFileName'),
),
+ // Charset for read
'utf-8',
+ // Read handler
(err, data) => {
if (err) {
res.status(500).send('Error returning offline page.');
return;
}
- const withNonce = data.replace('NONCE_TARGET', nonce);
- res.send(withNonce);
+ // We replace the placeholder with the actual nonce.
+ const offlinePageWithNonce = data.replace('OFFLINE_PAGE_NONCE_PLACEHOLDER', nonce);
+ // Send back the page as the response
+ res.send(offlinePageWithNonce);
},
);
}
diff --git a/server/middleware/reactApplication/ServerHTML.js b/server/middleware/reactApplication/ServerHTML.js
new file mode 100644
index 00000000..a8c860de
--- /dev/null
+++ b/server/middleware/reactApplication/ServerHTML.js
@@ -0,0 +1,148 @@
+/**
+ * This module is responsible for generating the HTML page response for
+ * the react application middleware.
+ */
+
+/* eslint-disable react/no-danger */
+/* eslint-disable react/no-array-index-key */
+
+import React, { Children } from 'react';
+import PropTypes from 'prop-types';
+import serialize from 'serialize-javascript';
+
+import config from '../../../config';
+import ifElse from '../../../shared/utils/logic/ifElse';
+import removeNil from '../../../shared/utils/arrays/removeNil';
+import getClientBundleEntryAssets from './getClientBundleEntryAssets';
+
+import ClientConfig from '../../../config/components/ClientConfig';
+import HTML from '../../../shared/components/HTML';
+
+// PRIVATES
+
+function KeyedComponent({ children }) {
+ return Children.only(children);
+}
+
+// Resolve the assets (js/css) for the client bundle's entry chunk.
+const clientEntryAssets = getClientBundleEntryAssets();
+
+function stylesheetTag(stylesheetFilePath) {
+ return (
+
+ );
+}
+
+function scriptTag(jsFilePath) {
+ return ;
+}
+
+// COMPONENT
+
+function ServerHTML(props) {
+ const {
+ asyncComponentsState,
+ helmet,
+ jobsState,
+ nonce,
+ reactAppString,
+ routerState,
+ storeState,
+ } = props;
+
+ // Creates an inline script definition that is protected by the nonce.
+ const inlineScript = body =>
+ ;
+
+ const headerElements = removeNil([
+ ...ifElse(helmet)(() => helmet.title.toComponent(), []),
+ ...ifElse(helmet)(() => helmet.base.toComponent(), []),
+ ...ifElse(helmet)(() => helmet.meta.toComponent(), []),
+ ...ifElse(helmet)(() => helmet.link.toComponent(), []),
+ ifElse(clientEntryAssets && clientEntryAssets.css)(() => stylesheetTag(clientEntryAssets.css)),
+ ...ifElse(helmet)(() => helmet.style.toComponent(), []),
+ ]);
+
+ const bodyElements = removeNil([
+ // Bind our redux store state so the client knows how to hydrate his one
+ ifElse(storeState)(() => inlineScript(`window.__APOLLO_STATE__=${serialize(storeState)};`)),
+
+ // Binds the client configuration object to the window object so
+ // that we can safely expose some configuration values to the
+ // client bundle that gets executed in the browser.
+ ,
+
+ // Bind our async components state so the client knows which ones
+ // to initialise so that the checksum matches the server response.
+ // @see https://github.com/ctrlplusb/react-async-component
+ ifElse(asyncComponentsState)(() =>
+ inlineScript(
+ `window.__ASYNC_COMPONENTS_REHYDRATE_STATE__=${serialize(asyncComponentsState)};`,
+ ),
+ ),
+
+ ifElse(routerState)(() => inlineScript(`window.__ROUTER_STATE__=${serialize(routerState)}`)),
+
+ // Enable the polyfill io script?
+ // This can't be configured within a react-helmet component as we
+ // may need the polyfill's before our client JS gets parsed.
+ ifElse(config('polyfillIO.enabled'))(() =>
+ scriptTag(
+ `https://cdn.polyfill.io/v2/polyfill.min.js?features=${config('polyfillIO.features').join(
+ ',',
+ )}`,
+ ),
+ ),
+ // When we are in development mode our development server will
+ // generate a vendor DLL in order to dramatically reduce our
+ // compilation times. Therefore we need to inject the path to the
+ // vendor dll bundle below.
+ ifElse(
+ process.env.BUILD_FLAG_IS_DEV === 'true' && config('bundles.client.devVendorDLL.enabled'),
+ )(() =>
+ scriptTag(
+ `${config('bundles.client.webPath')}${config(
+ 'bundles.client.devVendorDLL.name',
+ )}.js?t=${Date.now()}`,
+ ),
+ ),
+ ifElse(clientEntryAssets && clientEntryAssets.js)(() => scriptTag(clientEntryAssets.js)),
+ ...ifElse(helmet)(() => helmet.script.toComponent(), []),
+ ]);
+
+ return (
+ helmet.htmlAttributes.toComponent(), null)}
+ headerElements={headerElements.map((x, idx) =>
+ (
+ {x}
+ ),
+ )}
+ bodyElements={bodyElements.map((x, idx) =>
+ (
+ {x}
+ ),
+ )}
+ appBodyString={reactAppString}
+ />
+ );
+}
+
+ServerHTML.propTypes = {
+ // eslint-disable-next-line react/forbid-prop-types
+ asyncComponentsState: PropTypes.object,
+ // eslint-disable-next-line react/forbid-prop-types
+ helmet: PropTypes.object,
+ // eslint-disable-next-line react/forbid-prop-types
+ jobsState: PropTypes.object,
+ nonce: PropTypes.string,
+ reactAppString: PropTypes.string,
+ // eslint-disable-next-line react/forbid-prop-types
+ routerState: PropTypes.object,
+ // eslint-disable-next-line react/forbid-prop-types
+ storeState: PropTypes.object,
+};
+
+// EXPORT
+
+export default ServerHTML;
diff --git a/server/middleware/reactApplication/getClientBundleEntryAssets.js b/server/middleware/reactApplication/getClientBundleEntryAssets.js
new file mode 100644
index 00000000..83d4ab86
--- /dev/null
+++ b/server/middleware/reactApplication/getClientBundleEntryAssets.js
@@ -0,0 +1,52 @@
+/**
+ * This file resolves the entry assets available from our client bundle.
+ */
+
+import fs from 'fs';
+import { resolve as pathResolve } from 'path';
+import appRootDir from 'app-root-dir';
+import config from '../../../config';
+
+let resultCache;
+
+/**
+ * Retrieves the js/css for the named chunks that belong to our client bundle.
+ *
+ * Note: the order of the chunk names is important. The same ordering will be
+ * used when rendering the scripts.
+ *
+ * This is useful to us for a couple of reasons:
+ * - It allows us to target the assets for a specific chunk, thereby only
+ * loading the assets we know we will need for a specific request.
+ * - The assets are hashed, and therefore they can't be "manually" added
+ * to the render logic. Having this method allows us to easily fetch
+ * the respective assets simply by using a chunk name. :)
+ */
+export default function getClientBundleEntryAssets() {
+ // Return the assets json cache if it exists.
+ // In development mode we always read the assets json file from disk to avoid
+ // any cases where an older version gets cached.
+ if (process.env.BUILD_FLAG_IS_DEV === 'false' && resultCache) {
+ return resultCache;
+ }
+
+ const assetsFilePath = pathResolve(
+ appRootDir.get(),
+ config('bundles.client.outputPath'),
+ `./${config('bundleAssetsFileName')}`,
+ );
+
+ if (!fs.existsSync(assetsFilePath)) {
+ throw new Error(
+ `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.`,
+ );
+ }
+
+ const readAssetsJSONFile = () => JSON.parse(fs.readFileSync(assetsFilePath, 'utf8'));
+ const assetsJSONCache = readAssetsJSONFile();
+ if (typeof assetsJSONCache.index === 'undefined') {
+ throw new Error('No asset data found for expected "index" entry chunk of client bundle.');
+ }
+ resultCache = assetsJSONCache.index;
+ return resultCache;
+}
diff --git a/server/middleware/reactApplication/index.js b/server/middleware/reactApplication/index.js
new file mode 100644
index 00000000..accfade6
--- /dev/null
+++ b/server/middleware/reactApplication/index.js
@@ -0,0 +1,124 @@
+import React from "react";
+import { renderToString, renderToStaticMarkup } from "react-dom/server";
+import StaticRouter from "react-router-dom/StaticRouter";
+import {
+ AsyncComponentProvider,
+ createAsyncContext
+} from "react-async-component";
+import asyncBootstrapper from "react-async-bootstrapper";
+import Helmet from "react-helmet";
+import { ApolloProvider, getDataFromTree } from "react-apollo";
+import configureStore from "../../../shared/redux/configureStore";
+import {
+ createApolloClient,
+ getNetworkInterface
+} from "../../../shared/apollo";
+import config from "../../../config";
+import DemoApp from "../../../shared/components/DemoApp";
+import ServerHTML from "./ServerHTML";
+
+/**
+ * React application middleware, supports server side rendering.
+ */
+export default (async function reactApplicationMiddleware(request, response) {
+ // Ensure a nonce has been provided to us.
+ // See the server/middleware/security.js for more info.
+ if (typeof response.locals.nonce !== "string") {
+ throw new Error('A "nonce" value has not been attached to the response');
+ }
+ const nonce = response.locals.nonce;
+
+ // Apollo setup
+ // all options described below
+ // @see http://dev.apollodata.com/core/apollo-client-api.html#constructor
+ const clientOptions = {
+ // SSR mode prevents both the server and the client requesting the same data --
+ // stops you from making two requests for the same data.
+ ssrMode: true
+ };
+
+ // Network interface is responsible for fetching your data. It makes the request using
+ // the network connection.
+ // Using something like https://github.com/af/apollo-local-query or
+ // https://github.com/sysgears/persistgraphql-webpack-plugin allows you to perform
+ // queries without making a network request.
+ // Pass our headers to the networkInterface so that we can set headers / provide cookie or token.
+ const networkInterface = getNetworkInterface(clientOptions, request.headers);
+
+ const apolloClient = createApolloClient({
+ request,
+ clientOptions,
+ networkInterface
+ });
+
+ // It's possible to disable SSR, which can be useful in development mode.
+ // In this case traditional client side only rendering will occur.
+ if (config("disableSSR")) {
+ if (process.env.BUILD_FLAG_IS_DEV === "true") {
+ // eslint-disable-next-line no-console
+ console.log("==> Handling react route without SSR");
+ }
+ // SSR is disabled so we will return an "empty" html page and
+ // rely on the client to initialize and render the react application.
+ const html = renderToStaticMarkup();
+ response.status(200).send(`${html}`);
+ return;
+ }
+
+ // Create a context for our AsyncComponentProvider.
+ const asyncContext = createAsyncContext();
+
+ // Create a context for , which will allow us to
+ // query for the results of the render.
+ const reactRouterContext = {};
+
+ const initialState = {};
+ // Create the redux store.
+ const store = configureStore(apolloClient, initialState);
+
+ // Declare our React application.
+ const app = (
+
+
+
+
+
+
+
+ );
+
+ // Traverses entire React tree and determines which queries are needed to render, then
+ // fetches the data. It returns a promise which resolves when the data is ready in
+ // your Apollo Client store.
+ // @SEE http://dev.apollodata.com/react/server-side-rendering.html
+ await getDataFromTree(app);
+
+ // Pass our app into the react-async-component helper so that any async
+ // components are resolved for the render.
+ asyncBootstrapper(app).then(() => {
+ const appString = renderToString(app);
+
+ const html = renderToStaticMarkup(
+
+ );
+
+ // Check if the router context contains a redirect, if so we need to set
+ // the specific status and redirect header and end the response.
+ if (reactRouterContext.url) {
+ response.status(302).setHeader("Location", reactRouterContext.url);
+ response.end();
+ return;
+ }
+
+ response
+ .status(reactRouterContext.status || 200)
+ .send(`${html}`);
+ });
+});
diff --git a/src/server/middleware/security.js b/server/middleware/security.js
similarity index 83%
rename from src/server/middleware/security.js
rename to server/middleware/security.js
index 485c4a0b..d5361b84 100644
--- a/src/server/middleware/security.js
+++ b/server/middleware/security.js
@@ -1,7 +1,7 @@
import uuid from 'uuid';
import hpp from 'hpp';
import helmet from 'helmet';
-import config from '../../../config';
+import config from '../../config';
const cspConfig = {
directives: {
@@ -17,22 +17,24 @@ const cspConfig = {
// need the following:
// 'data:',
],
- fontSrc: ["'self'"],
+ fontSrc: ["'self'", 'data:'],
objectSrc: ["'self'"],
mediaSrc: ["'self'"],
manifestSrc: ["'self'"],
scriptSrc: [
// Allow scripts hosted from our application.
"'self'",
- // Allow scripts from cdn.polyfill.io so that we can import the polyfill.
- 'cdn.polyfill.io',
// Note: We will execution of any inline scripts that have the following
// nonce identifier attached to them.
// This is useful for guarding your application whilst allowing an inline
// script to do data store rehydration (redux/mobx/apollo) for example.
// @see https://helmetjs.github.io/docs/csp/
- // $FlowFixMe
(req, res) => `'nonce-${res.locals.nonce}'`,
+ // This is a know workaround for browsers that don't support nonces.
+ // It will be ignored by browsers that do support nonces as they will
+ // recognise that we have also provided a nonce configuration and
+ // use the stricter rule.
+ "'unsafe-inline'",
],
styleSrc: [
"'self'",
@@ -44,22 +46,20 @@ const cspConfig = {
};
// Add any additional CSP from the static config.
-Object.keys(config.cspExtensions).forEach((key) => {
+const cspExtensions = config('cspExtensions');
+Object.keys(cspExtensions).forEach((key) => {
if (cspConfig.directives[key]) {
- cspConfig.directives[key] = cspConfig.directives[key]
- .concat(config.cspExtensions[key]);
+ cspConfig.directives[key] = cspConfig.directives[key].concat(cspExtensions[key]);
} else {
- cspConfig.directives[key] = config.cspExtensions[key];
+ cspConfig.directives[key] = cspExtensions[key];
}
});
-if (process.env.NODE_ENV === 'development') {
+if (process.env.BUILD_FLAG_IS_DEV === 'true') {
// When in development mode we need to add our secondary express server that
// is used to host our client bundle to our csp config.
Object.keys(cspConfig.directives).forEach((directive) => {
- cspConfig.directives[directive].push(
- `${config.host}:${config.clientDevServerPort}`,
- );
+ cspConfig.directives[directive].push(`${config('host')}:${config('clientDevServerPort')}`);
});
}
@@ -119,7 +119,8 @@ const securityMiddleware = [
// 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.
- helmet.contentSecurityPolicy(cspConfig),
+
+ // helmet.contentSecurityPolicy(cspConfig),
];
export default securityMiddleware;
diff --git a/src/server/middleware/serviceWorker.js b/server/middleware/serviceWorker.js
similarity index 64%
rename from src/server/middleware/serviceWorker.js
rename to server/middleware/serviceWorker.js
index e743fc38..b414b768 100644
--- a/src/server/middleware/serviceWorker.js
+++ b/server/middleware/serviceWorker.js
@@ -2,15 +2,15 @@
import { resolve as pathResolve } from 'path';
import appRootDir from 'app-root-dir';
-import config from '../../../config';
+import config from '../../config';
-// Middleware to server our service worker.
+// Middleware to serve our service worker.
function serviceWorkerMiddleware(req, res, next) {
res.sendFile(
pathResolve(
appRootDir.get(),
- config.bundles.client.outputPath,
- config.serviceWorker.fileName,
+ config('bundles.client.outputPath'),
+ config('serviceWorker.fileName'),
),
);
}
diff --git a/shared/README.md b/shared/README.md
new file mode 100644
index 00000000..f9bcad2e
--- /dev/null
+++ b/shared/README.md
@@ -0,0 +1,3 @@
+# src/shared
+
+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).
diff --git a/shared/apollo/createApolloClient.js b/shared/apollo/createApolloClient.js
new file mode 100644
index 00000000..8890bfad
--- /dev/null
+++ b/shared/apollo/createApolloClient.js
@@ -0,0 +1,34 @@
+import { ApolloClient } from "react-apollo";
+
+// The ApolloClient takes its options as well as a network interface.
+function createApolloClient({ clientOptions = {}, networkInterface }) {
+ if (!networkInterface) {
+ throw Error("Please pass a network interface to be used on apollo client");
+ }
+ const options = Object.assign(
+ {},
+ {
+ addTypeName: true,
+ // DataIdFromObject is used by Apollo to identify unique entities from
+ // your queries.
+ dataIdFromObject: result =>
+ // you might see o => o.id commonly with Apollo.
+ // If the IDs are only unique per type (this is typical if an ID is just an
+ // ID out of a database table), you can use the `__typename` field to scope it.
+ // This is a GraphQL field that's automatically available, but you do need
+ // to query for it also. @SEE: http://dev.apollodata.com/angular2/cache-updates.html#dataIdFromObject
+ // eslint-disable-next-line no-underscore-dangle
+ result.id && result.__typename
+ ? `${result.__typename}${result.id}`
+ : null
+ },
+ clientOptions
+ );
+
+ return new ApolloClient({
+ networkInterface,
+ ...options
+ });
+}
+
+export default createApolloClient;
diff --git a/shared/apollo/hybridNetworkInterface.js b/shared/apollo/hybridNetworkInterface.js
new file mode 100644
index 00000000..b5897110
--- /dev/null
+++ b/shared/apollo/hybridNetworkInterface.js
@@ -0,0 +1,61 @@
+import {
+ createBatchingNetworkInterface,
+ createNetworkInterface
+} from "apollo-client";
+import merge from "lodash.merge";
+import config from "../../config";
+
+// The network interface is created for both the server and the client. For
+// the most part they are the same, but have a few distinct properties specific
+// to the environment (browser/server).
+// --/
+// The "hybrid" network interface we are creating here combines both interfaces for us.
+// 1. Regular requests
+// 2. Batch requests
+// You can only use one type at a time, but switching is easy.
+// Batching tells Apollo to combine multiple queries into a single request provided
+// they are made within a specified amount of time.
+// @SEE http://dev.apollodata.com/core/network.html#query-batching
+export class HTTPHybridNetworkInterface {
+ constructor(opts = {}, headers = {}) {
+ const richerOpts = merge(
+ {},
+ {
+ uri: config("graphqlUri"),
+ opts: {
+ headers
+ }
+ },
+ opts
+ );
+
+ this.batchedInterface = createBatchingNetworkInterface(richerOpts);
+ this.networkInterface = createNetworkInterface(richerOpts);
+ }
+
+ query(request) {
+ // eslint-disable-next-line no-underscore-dangle
+ if (request.variables && request.variables.__disableBatch) {
+ return this.networkInterface.query(request);
+ }
+
+ return this.batchedInterface.query(request);
+ }
+ // Network interfaces have their own specific middleware and it's vital
+ // to use the batching middleware for the batched network interface.
+ use(middlewares) {
+ this.networkInterface.use(middlewares);
+ this.batchedInterface.use(middlewares);
+ return this;
+ }
+
+ useAfter(afterwares) {
+ this.networkInterface.useAfter(afterwares);
+ this.batchedInterface.useAfter(afterwares);
+ return this;
+ }
+}
+
+export function createHybridNetworkInterface(opts = {}, headers = {}) {
+ return new HTTPHybridNetworkInterface(opts, headers);
+}
diff --git a/shared/apollo/index.js b/shared/apollo/index.js
new file mode 100644
index 00000000..0ba67a07
--- /dev/null
+++ b/shared/apollo/index.js
@@ -0,0 +1,2 @@
+export { default as createApolloClient } from './createApolloClient';
+export { default as getNetworkInterface } from './transport';
diff --git a/shared/apollo/transport.js b/shared/apollo/transport.js
new file mode 100644
index 00000000..074ecf4b
--- /dev/null
+++ b/shared/apollo/transport.js
@@ -0,0 +1,28 @@
+import { createNetworkInterface } from "react-apollo";
+import merge from "lodash.merge";
+import config from "../../config";
+import { createHybridNetworkInterface } from "./hybridNetworkInterface";
+
+function createSimpleNetworkInterface(opts = {}, headers = {}) {
+ const richerOpts = merge(
+ {},
+ {
+ uri: config("graphqlUri"),
+ opts: {
+ headers
+ }
+ },
+ opts
+ );
+
+ return createNetworkInterface(richerOpts);
+}
+
+function getNetworkInterface(opts = {}, headers = {}) {
+ // Enable or disable query batching within your config file.
+ return config("graphqlBatch")
+ ? createHybridNetworkInterface(opts, headers)
+ : createSimpleNetworkInterface(opts, headers);
+}
+
+export default getNetworkInterface;
diff --git a/shared/components/DemoApp/AsyncAboutRoute/AboutRoute.js b/shared/components/DemoApp/AsyncAboutRoute/AboutRoute.js
new file mode 100644
index 00000000..78c88eb8
--- /dev/null
+++ b/shared/components/DemoApp/AsyncAboutRoute/AboutRoute.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import Helmet from 'react-helmet';
+
+function AboutRoute() {
+ return (
+
+
+ About
+
+
+
Produced with โค๏ธ
+
+
+ View our contributors list on our
+ {' '}
+ GitHub
+ {' '}
+ page.
+
+ View our contributors list on our
+
+
+ GitHub
+
+
+ page.
+
+
+`;
diff --git a/shared/components/DemoApp/AsyncAboutRoute/index.js b/shared/components/DemoApp/AsyncAboutRoute/index.js
new file mode 100644
index 00000000..293cfa0e
--- /dev/null
+++ b/shared/components/DemoApp/AsyncAboutRoute/index.js
@@ -0,0 +1,6 @@
+import { asyncComponent } from 'react-async-component';
+
+export default asyncComponent({
+ // include home and about route in same chunk e.g main
+ resolve: () => System.import(/* webpackChunkName: "main" */ './AboutRoute'),
+});
diff --git a/src/shared/components/DemoApp/Home/Home.js b/shared/components/DemoApp/AsyncHomeRoute/HomeRoute.js
similarity index 63%
rename from src/shared/components/DemoApp/Home/Home.js
rename to shared/components/DemoApp/AsyncHomeRoute/HomeRoute.js
index 056c6d50..ffe8b979 100644
--- a/src/shared/components/DemoApp/Home/Home.js
+++ b/shared/components/DemoApp/AsyncHomeRoute/HomeRoute.js
@@ -1,13 +1,16 @@
import React from 'react';
import Helmet from 'react-helmet';
-import { safeConfigGet } from '../../../utils/config';
-function Home() {
+import config from '../../../../config';
+
+function HomeRoute() {
return (
-
-
+
+
+ Home
+
-
{safeConfigGet(['welcomeMessage'])}
+
{config('welcomeMessage')}
This starter kit contains all the build tooling and configuration you
@@ -15,8 +18,8 @@ function Home() {
minimal project set up allowing you to make your own architecture
decisions (Redux/Mobx etc).
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).
-
+
`;
diff --git a/shared/components/DemoApp/AsyncHomeRoute/index.js b/shared/components/DemoApp/AsyncHomeRoute/index.js
new file mode 100644
index 00000000..5cdff462
--- /dev/null
+++ b/shared/components/DemoApp/AsyncHomeRoute/index.js
@@ -0,0 +1,6 @@
+import { asyncComponent } from 'react-async-component';
+
+export default asyncComponent({
+ // include home and about route in same chunk e.g main
+ resolve: () => System.import(/* webpackChunkName: "main" */ './HomeRoute'),
+});
diff --git a/shared/components/DemoApp/AsyncPostsRoute/Post/Post.js b/shared/components/DemoApp/AsyncPostsRoute/Post/Post.js
new file mode 100644
index 00000000..0af7e1d1
--- /dev/null
+++ b/shared/components/DemoApp/AsyncPostsRoute/Post/Post.js
@@ -0,0 +1,61 @@
+/* eslint-disable react/prefer-stateless-function */
+import React, { Component } from 'react';
+import { shape, bool, string, number } from 'prop-types';
+import { gql, graphql } from 'react-apollo';
+import Helmet from 'react-helmet';
+// import SINGLE_POST_QUERY from './singlePost.graphql';
+
+export class Post extends Component {
+ render() {
+ const { data: { loading, singlePost } } = this.props;
+ if (loading) {
+ return
+`;
diff --git a/shared/components/DemoApp/AsyncPostsRoute/Post/index.js b/shared/components/DemoApp/AsyncPostsRoute/Post/index.js
new file mode 100644
index 00000000..195b93ec
--- /dev/null
+++ b/shared/components/DemoApp/AsyncPostsRoute/Post/index.js
@@ -0,0 +1,7 @@
+import { asyncComponent } from 'react-async-component';
+
+export default asyncComponent({
+ resolve: () => System.import('./Post'),
+ ssrMode: 'boundary',
+ name: 'Post',
+});
diff --git a/shared/components/DemoApp/AsyncPostsRoute/Post/singlePost.graphql b/shared/components/DemoApp/AsyncPostsRoute/Post/singlePost.graphql
new file mode 100644
index 00000000..f70dc3b3
--- /dev/null
+++ b/shared/components/DemoApp/AsyncPostsRoute/Post/singlePost.graphql
@@ -0,0 +1,8 @@
+query singlePost($id: Int!) {
+ singlePost(id: $id) {
+ id
+ title
+ userId
+ body
+ }
+}
diff --git a/shared/components/DemoApp/AsyncPostsRoute/Posts.js b/shared/components/DemoApp/AsyncPostsRoute/Posts.js
new file mode 100644
index 00000000..c368e54f
--- /dev/null
+++ b/shared/components/DemoApp/AsyncPostsRoute/Posts.js
@@ -0,0 +1,60 @@
+/* eslint-disable react/prefer-stateless-function */
+import React, { Component } from 'react';
+import { shape, bool, object } from 'prop-types';
+import { gql, graphql } from 'react-apollo';
+import Link from 'react-router-dom/Link';
+import Helmet from 'react-helmet';
+
+// import POSTS_LIST_QUERY from './postList.graphql';
+
+class Posts extends Component {
+ render() {
+ const { data: { loading, postList } } = this.props;
+ if (loading) {
+ return Loading...;
+ }
+ return (
+
+
+
+
+ {postList.posts.map(post =>
+
+
+
+ {post.title}
+
+
+ {post.body}
+
+
+ Read more...
+
+
,
+ )}
+
+
+
+ );
+ }
+}
+
+Posts.propTypes = {
+ data: shape({
+ loading: bool,
+ postList: object,
+ }).isRequired,
+};
+export const POSTS_LIST_QUERY = gql`
+ query {
+ postList {
+ posts {
+ id
+ title
+ body
+ userId
+ }
+ }
+ }
+`;
+export default graphql(POSTS_LIST_QUERY)(Posts);
diff --git a/shared/components/DemoApp/AsyncPostsRoute/index.js b/shared/components/DemoApp/AsyncPostsRoute/index.js
new file mode 100644
index 00000000..6d18b43d
--- /dev/null
+++ b/shared/components/DemoApp/AsyncPostsRoute/index.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import Route from 'react-router-dom/Route';
+import Switch from 'react-router-dom/Switch';
+import Post from './Post';
+import Posts from './Posts';
+
+const AsyncPostsRoute = () =>
+
+
+
+
+
+
;
+
+export default AsyncPostsRoute;
diff --git a/shared/components/DemoApp/AsyncPostsRoute/postList.graphql b/shared/components/DemoApp/AsyncPostsRoute/postList.graphql
new file mode 100644
index 00000000..2d3d003e
--- /dev/null
+++ b/shared/components/DemoApp/AsyncPostsRoute/postList.graphql
@@ -0,0 +1,10 @@
+query {
+ postList {
+ posts {
+ id
+ title
+ body
+ userId
+ }
+ }
+}
diff --git a/src/shared/components/DemoApp/Error404/Error404.test.js b/shared/components/DemoApp/Error404/__tests__/Error404.test.js
similarity index 55%
rename from src/shared/components/DemoApp/Error404/Error404.test.js
rename to shared/components/DemoApp/Error404/__tests__/Error404.test.js
index a00fe517..31bb8eb5 100644
--- a/src/shared/components/DemoApp/Error404/Error404.test.js
+++ b/shared/components/DemoApp/Error404/__tests__/Error404.test.js
@@ -2,11 +2,14 @@
import React from 'react';
import { shallow } from 'enzyme';
-import Error404 from './Error404';
+
+import Error404 from '../index';
describe('', () => {
test('renders', () => {
- const wrapper = shallow();
+ const staticContext = {};
+ const wrapper = shallow();
expect(wrapper).toMatchSnapshot();
+ expect(staticContext.missed).toBeTruthy();
});
});
diff --git a/src/shared/components/DemoApp/Error404/__snapshots__/Error404.test.js.snap b/shared/components/DemoApp/Error404/__tests__/__snapshots__/Error404.test.js.snap
similarity index 66%
rename from src/shared/components/DemoApp/Error404/__snapshots__/Error404.test.js.snap
rename to shared/components/DemoApp/Error404/__tests__/__snapshots__/Error404.test.js.snap
index 070a96b1..007ceddc 100644
--- a/src/shared/components/DemoApp/Error404/__snapshots__/Error404.test.js.snap
+++ b/shared/components/DemoApp/Error404/__tests__/__snapshots__/Error404.test.js.snap
@@ -1,3 +1,5 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
exports[` renders 1`] = `
Sorry, that page was not found.
diff --git a/shared/components/DemoApp/Error404/index.js b/shared/components/DemoApp/Error404/index.js
new file mode 100644
index 00000000..290cf714
--- /dev/null
+++ b/shared/components/DemoApp/Error404/index.js
@@ -0,0 +1,26 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+
+class Error404 extends Component {
+ componentWillMount() {
+ const { staticContext } = this.props;
+ if (staticContext) {
+ staticContext.missed = true;
+ }
+ }
+
+ render() {
+ return
Sorry, that page was not found.
;
+ }
+}
+
+Error404.propTypes = {
+ // eslint-disable-next-line react/forbid-prop-types
+ staticContext: PropTypes.object,
+};
+
+Error404.defaultProps = {
+ staticContext: {},
+};
+
+export default Error404;
diff --git a/src/shared/components/DemoApp/Header/Logo/Logo.js b/shared/components/DemoApp/Header/Logo/index.js
similarity index 56%
rename from src/shared/components/DemoApp/Header/Logo/Logo.js
rename to shared/components/DemoApp/Header/Logo/index.js
index 0c91a2b2..11907d21 100644
--- a/src/shared/components/DemoApp/Header/Logo/Logo.js
+++ b/shared/components/DemoApp/Header/Logo/index.js
@@ -2,9 +2,7 @@ import React from 'react';
import logo from './logo.png';
function Logo() {
- return (
-
- );
+ return ;
}
export default Logo;
diff --git a/src/shared/components/DemoApp/Header/Logo/logo.png b/shared/components/DemoApp/Header/Logo/logo.png
similarity index 100%
rename from src/shared/components/DemoApp/Header/Logo/logo.png
rename to shared/components/DemoApp/Header/Logo/logo.png
diff --git a/src/shared/components/DemoApp/Header/Menu/Menu.test.js b/shared/components/DemoApp/Header/Menu/__tests__/Menu.test.js
similarity index 89%
rename from src/shared/components/DemoApp/Header/Menu/Menu.test.js
rename to shared/components/DemoApp/Header/Menu/__tests__/Menu.test.js
index f344832e..a578cc2e 100644
--- a/src/shared/components/DemoApp/Header/Menu/Menu.test.js
+++ b/shared/components/DemoApp/Header/Menu/__tests__/Menu.test.js
@@ -2,7 +2,8 @@
import React from 'react';
import { shallow } from 'enzyme';
-import Menu from './Menu';
+
+import Menu from '../index';
describe('', () => {
test('renders', () => {
diff --git a/shared/components/DemoApp/Header/Menu/__tests__/__snapshots__/Menu.test.js.snap b/shared/components/DemoApp/Header/Menu/__tests__/__snapshots__/Menu.test.js.snap
new file mode 100644
index 00000000..12aed177
--- /dev/null
+++ b/shared/components/DemoApp/Header/Menu/__tests__/__snapshots__/Menu.test.js.snap
@@ -0,0 +1,38 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` renders 1`] = `
+
+
+
+ Home
+
+
+
+
+ Posts
+
+
+
+
+ About
+
+
+
+`;
diff --git a/src/shared/components/DemoApp/Header/Menu/Menu.js b/shared/components/DemoApp/Header/Menu/index.js
similarity index 51%
rename from src/shared/components/DemoApp/Header/Menu/Menu.js
rename to shared/components/DemoApp/Header/Menu/index.js
index 0c48c630..b29a6435 100644
--- a/src/shared/components/DemoApp/Header/Menu/Menu.js
+++ b/shared/components/DemoApp/Header/Menu/index.js
@@ -1,10 +1,11 @@
import React from 'react';
-import { Link } from 'react-router';
+import Link from 'react-router-dom/Link';
function Menu() {
return (
-
+
Home
+
Posts
About
);
diff --git a/src/shared/components/DemoApp/Header/Header.js b/shared/components/DemoApp/Header/index.js
similarity index 75%
rename from src/shared/components/DemoApp/Header/Header.js
rename to shared/components/DemoApp/Header/index.js
index 187358e7..b17470a4 100644
--- a/src/shared/components/DemoApp/Header/Header.js
+++ b/shared/components/DemoApp/Header/index.js
@@ -8,7 +8,7 @@ function Header() {
React, Universally
- A starter kit giving you the minimum requirements for a modern universal react application.
+ A starter kit for universal react applications.
diff --git a/src/shared/components/DemoApp/globals.css b/shared/components/DemoApp/globals.css
similarity index 100%
rename from src/shared/components/DemoApp/globals.css
rename to shared/components/DemoApp/globals.css
diff --git a/shared/components/DemoApp/index.js b/shared/components/DemoApp/index.js
new file mode 100644
index 00000000..add21857
--- /dev/null
+++ b/shared/components/DemoApp/index.js
@@ -0,0 +1,127 @@
+import 'normalize.css/normalize.css';
+
+import React from 'react';
+import Switch from 'react-router-dom/Switch';
+import Route from 'react-router-dom/Route';
+import Helmet from 'react-helmet';
+
+import config from '../../../config';
+
+import './globals.css';
+
+import Error404 from './Error404';
+import Header from './Header';
+
+import AsyncHomeRoute from './AsyncHomeRoute';
+import AsyncPostsRoute from './AsyncPostsRoute';
+import AsyncAboutRoute from './AsyncAboutRoute';
+
+function DemoApp() {
+ return (
+
+
+
+ {config('htmlPage.defaultTitle')}
+
+
+
+
+
+
+
+
+ {/*
+ A great reference for favicons:
+ https://github.com/audreyr/favicon-cheat-sheet
+ It's a pain to manage/generate them. I run both these in order,
+ and combine their results:
+ http://realfavicongenerator.net/
+ http://www.favicomatic.com/
+ */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/*
+ NOTE: This is simply for quick and easy styling on the demo. Remove
+ this and the related items from the Content Security Policy in the
+ global config if you have no intention of using milligram.
+ */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default DemoApp;
diff --git a/shared/components/HTML/index.js b/shared/components/HTML/index.js
new file mode 100644
index 00000000..711643c6
--- /dev/null
+++ b/shared/components/HTML/index.js
@@ -0,0 +1,43 @@
+/* eslint-disable react/no-danger */
+/* eslint-disable jsx-a11y/html-has-lang */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+/**
+ * The is the HTML shell for our React Application.
+ */
+function HTML(props) {
+ const { htmlAttributes, headerElements, bodyElements, appBodyString } = props;
+
+ return (
+
+
+ {headerElements}
+
+
+
+ {bodyElements}
+
+
+ );
+}
+
+HTML.propTypes = {
+ // eslint-disable-next-line react/forbid-prop-types
+ htmlAttributes: PropTypes.object,
+ headerElements: PropTypes.node,
+ bodyElements: PropTypes.node,
+ appBodyString: PropTypes.string,
+};
+
+HTML.defaultProps = {
+ htmlAttributes: null,
+ headerElements: null,
+ bodyElements: null,
+ appBodyString: '',
+};
+
+// EXPORT
+
+export default HTML;
diff --git a/shared/reducers/index.js b/shared/reducers/index.js
new file mode 100644
index 00000000..3e86ef8d
--- /dev/null
+++ b/shared/reducers/index.js
@@ -0,0 +1,10 @@
+import { combineReducers } from 'redux';
+
+// -----------------------------------------------------------------------------
+// REDUCER EXPORT
+
+export default function getReducers(apolloClient) {
+ return combineReducers({
+ apollo: apolloClient.reducer(),
+ });
+}
diff --git a/shared/redux/configureStore.js b/shared/redux/configureStore.js
new file mode 100644
index 00000000..b95711a1
--- /dev/null
+++ b/shared/redux/configureStore.js
@@ -0,0 +1,67 @@
+import { createStore, applyMiddleware, compose } from 'redux';
+import thunk from 'redux-thunk';
+import axios from 'axios';
+import getReducers from '../reducers';
+
+function configureStore(apolloClient, initialState) {
+ const enhancers = compose(
+ // Middleware store enhancer.
+ applyMiddleware(
+ // Initialising redux-thunk with extra arguments will pass the below
+ // arguments to all the redux-thunk actions. Below we are passing a
+ // preconfigured axios instance which can be used to fetch data with.
+ // @see https://github.com/gaearon/redux-thunk
+ thunk.withExtraArgument({ axios }),
+ apolloClient.middleware(),
+ ),
+ // Redux Dev Tools store enhancer.
+ // @see https://github.com/zalmoxisus/redux-devtools-extension
+ // We only want this enhancer enabled for development and when in a browser
+ // with the extension installed.
+ process.env.NODE_ENV === 'development' &&
+ typeof window !== 'undefined' &&
+ typeof window.devToolsExtension !== 'undefined'
+ ? // Call the brower extension function to create the enhancer.
+ window.devToolsExtension()
+ : // Else we return a no-op function.
+ f => f,
+ );
+
+ const store = initialState
+ ? createStore(getReducers(apolloClient), initialState, enhancers)
+ : createStore(getReducers(apolloClient), enhancers);
+
+ if (process.env.NODE_ENV === 'development' && module.hot) {
+ // Enable Webpack hot module replacement for reducers. This is so that we
+ // don't lose all of our current application state during hot reloading.
+ module.hot.accept('../reducers', () => {
+ const nextRootReducer = require('../reducers').default; // eslint-disable-line global-require
+
+ store.replaceReducer(nextRootReducer);
+ });
+ }
+
+ return store;
+}
+
+// NOTE: If we create an '/api' endpoint in our application then we will neeed to
+// configure the axios instances so that they will resolve to the proper URL
+// endpoints on the server. We have to provide absolute URLs for any of our
+// server bundles. To do so we can set the default 'baseURL' for axios. Any
+// relative path requests will then be appended to the 'baseURL' in order to
+// form an absolute URL.
+// We don't need to worry about this for client side executions, relative paths
+// will work fine there.
+// Example:
+//
+// const axiosConfig = process.env.IS_NODE === true
+// ? { baseURL: process.env.NOW_URL || notEmpty(process.env.SERVER_URL) }
+// : {};
+//
+// Then we will then have to initialise our redux-thunk middlware like so:
+//
+// thunk.withExtraArgument({
+// axios: axios.create(axiosConfig),
+// })
+
+export default configureStore;
diff --git a/shared/utils/arrays/index.js b/shared/utils/arrays/index.js
new file mode 100644
index 00000000..dc5ea89f
--- /dev/null
+++ b/shared/utils/arrays/index.js
@@ -0,0 +1,5 @@
+/* eslint-disable import/prefer-default-export */
+
+import removeNil from './removeNil';
+
+export { removeNil };
diff --git a/shared/utils/arrays/removeNil.js b/shared/utils/arrays/removeNil.js
new file mode 100644
index 00000000..d2b4c81f
--- /dev/null
+++ b/shared/utils/arrays/removeNil.js
@@ -0,0 +1,10 @@
+/**
+ * Filters out all null/undefined items from the given array.
+ *
+ * @param {Array} as - the target array
+ *
+ * @return {Array} The filtered array.
+ */
+export default function removeNil(as) {
+ return as.filter(a => a != null);
+}
diff --git a/shared/utils/logic/ifElse.js b/shared/utils/logic/ifElse.js
new file mode 100644
index 00000000..0a2b811d
--- /dev/null
+++ b/shared/utils/logic/ifElse.js
@@ -0,0 +1,21 @@
+const execIfFunc = x => (typeof x === 'function' ? x() : x);
+
+/**
+ * This is a higher order function that accepts a boolean condition and will
+ * return a function allowing you to provide if/else values that should be
+ * resolved based on the boolean condition.
+ *
+ * @param {Boolean|() => Boolean} condition:
+ * The condition to test against. This can be a function for lazy resolution.
+ *
+ * @return {(X|() => X, Y|() => Y) => X|Y}
+ * A function where the first paramater is the "if" and the second paramater
+ * is the "else". Each of these allows lazy resolving by providing a function.
+ *
+ * @example
+ * const ifDev = ifElse(process.env.NODE_ENV === 'development');
+ * ifDev('foo', () => 'lazy resolved'); // => 'foo'
+ */
+export default function ifElse(condition) {
+ return (then, or) => (execIfFunc(condition) ? execIfFunc(then) : execIfFunc(or));
+}
diff --git a/shared/utils/logic/index.js b/shared/utils/logic/index.js
new file mode 100644
index 00000000..21078931
--- /dev/null
+++ b/shared/utils/logic/index.js
@@ -0,0 +1,5 @@
+/* eslint-disable import/prefer-default-export */
+
+import ifElse from './ifElse';
+
+export { ifElse };
diff --git a/shared/utils/objects/filterWithRules.js b/shared/utils/objects/filterWithRules.js
new file mode 100644
index 00000000..613ff0a1
--- /dev/null
+++ b/shared/utils/objects/filterWithRules.js
@@ -0,0 +1,56 @@
+function filterWithRulesLoop(rules, obj, basePropPath = '') {
+ return Object.keys(rules).reduce(
+ (acc, key) => {
+ const propPath = basePropPath !== '' ? `${basePropPath}.${key}` : key;
+
+ if (typeof rules[key] === 'object') {
+ if (typeof obj[key] !== 'object') {
+ throw new Error(`Expected prop at path "${propPath}" to be an object`);
+ }
+ acc[key] = filterWithRulesLoop(rules[key], obj[key], propPath); // eslint-disable-line no-param-reassign,max-len
+ } else if (rules[key]) {
+ if (typeof obj[key] === 'undefined') {
+ throw new Error(
+ `Filter set an "allow" on path "${propPath}", however, this path was not found on the source object.`,
+ );
+ }
+ acc[key] = obj[key]; // eslint-disable-line no-param-reassign
+ }
+ return acc;
+ },
+ {},
+ );
+}
+
+/**
+ * Applies a rules object to filter a given object's structure.
+ *
+ * The rules object should match the shape of the source object and should
+ * have a truthy/falsey value indicating if a property should be included/
+ * excluded. If the filters do not contain a property that exists on the
+ * source object then the respective property will be excluded.
+ *
+ * @param {Object} rules : The filter rules.
+ * @param {Object} obj : The object to filter.
+ *
+ * @return {Object}
+ * The filtered object.
+ *
+ * @example
+ * filter(
+ * // rules
+ * {
+ * foo: { bar: true },
+ * poop: true
+ * },
+ * // source
+ * {
+ * foo: { bar: 'bar', qux: 'qux' },
+ * bob: 'bob',
+ * poop: { plop: 'splash' }
+ * },
+ * )
+ */
+export default function filterWithRules(rules, obj) {
+ return filterWithRulesLoop(rules, obj);
+}
diff --git a/shared/utils/objects/index.js b/shared/utils/objects/index.js
new file mode 100644
index 00000000..a36b38a6
--- /dev/null
+++ b/shared/utils/objects/index.js
@@ -0,0 +1,4 @@
+import filterWithRules from './filterWithRules';
+import mergeDeep from './mergeDeep';
+
+export { filterWithRules, mergeDeep };
diff --git a/shared/utils/objects/mergeDeep.js b/shared/utils/objects/mergeDeep.js
new file mode 100644
index 00000000..6d30c935
--- /dev/null
+++ b/shared/utils/objects/mergeDeep.js
@@ -0,0 +1,35 @@
+import removeNil from '../arrays/removeNil';
+
+/**
+ * Deeply merges a given set of objects together.
+ *
+ * Objects to the right take priority.
+ *
+ * @param {...Object} args - The objects to merge.
+ *
+ * @return {Object} - The merged object.
+ */
+export default function mergeDeep(...args) {
+ const filtered = removeNil(args);
+ if (filtered.length < 1) {
+ return {};
+ }
+ if (filtered.length === 1) {
+ return args[0];
+ }
+ return filtered.reduce(
+ (acc, cur) => {
+ Object.keys(cur).forEach((key) => {
+ if (typeof acc[key] === 'object' && typeof cur[key] === 'object') {
+ // eslint-disable-next-line no-param-reassign
+ acc[key] = mergeDeep(acc[key], cur[key]);
+ } else {
+ // eslint-disable-next-line no-param-reassign
+ acc[key] = cur[key];
+ }
+ });
+ return acc;
+ },
+ {},
+ );
+}
diff --git a/src/client/index.js b/src/client/index.js
deleted file mode 100644
index 8f2789d2..00000000
--- a/src/client/index.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/* eslint-disable global-require */
-
-import React from 'react';
-import { render } from 'react-dom';
-import { BrowserRouter } from 'react-router';
-import { CodeSplitProvider, rehydrateState } from 'code-split-component';
-import ReactHotLoader from './components/ReactHotLoader';
-import DemoApp from '../shared/components/DemoApp';
-
-// Get the DOM Element that will host our React application.
-const container = document.querySelector('#app');
-
-function renderApp(TheApp) {
- // We use the code-split-component library to provide us with code splitting
- // within our application. This library supports server rendered applications,
- // but for server rendered applications it requires that we rehydrate any
- // code split modules that may have been rendered for a request. We use
- // the provided helper and then pass the result to the CodeSplitProvider
- // instance which takes care of the rest for us. This is really important
- // to do as it will ensure that our React checksum for the client will match
- // the content returned by the server.
- // @see https://github.com/ctrlplusb/code-split-component
- rehydrateState().then(codeSplitState =>
- render(
-
-
-
-
-
-
- ,
- container,
- ),
- );
-}
-
-// The following is needed so that we can support hot reloading our application.
-if (process.env.NODE_ENV === 'development' && module.hot) {
- // Accept changes to this file for hot reloading.
- module.hot.accept('./index.js');
- // Any changes to our App will cause a hotload re-render.
- module.hot.accept(
- '../shared/components/DemoApp',
- () => renderApp(require('../shared/components/DemoApp').default),
- );
-}
-
-// Execute the first render of our app.
-renderApp(DemoApp);
-
-// This registers our service worker for asset caching and offline support.
-// Keep this as the last item, just in case the code execution failed (thanks
-// to react-boilerplate for that tip.)
-require('./registerServiceWorker');
diff --git a/src/server/index.js b/src/server/index.js
deleted file mode 100644
index 5570f733..00000000
--- a/src/server/index.js
+++ /dev/null
@@ -1,61 +0,0 @@
-/* eslint-disable no-console */
-
-import express from 'express';
-import compression from 'compression';
-import { resolve as pathResolve } from 'path';
-import appRootDir from 'app-root-dir';
-import reactApplication from './middleware/reactApplication';
-import security from './middleware/security';
-import clientBundle from './middleware/clientBundle';
-import serviceWorker from './middleware/serviceWorker';
-import offlinePage from './middleware/offlinePage';
-import errorHandlers from './middleware/errorHandlers';
-import config from '../../config';
-
-// Create our express based server.
-const app = express();
-
-// Don't expose any software information to potential hackers.
-app.disable('x-powered-by');
-
-// Security middlewares.
-app.use(...security);
-
-// Gzip compress the responses.
-app.use(compression());
-
-// When in production mode, we will serve our service worker which was generated
-// by the offline-plugin webpack plugin. See the webpack plugins section for
-// more information.
-// Note: the service worker needs to be served from the http root of your
-// application for it to work correctly.
-if (process.env.NODE_ENV === 'production'
- && config.serviceWorker.enabled) {
- app.get(`/${config.serviceWorker.fileName}`, serviceWorker);
- app.get(
- `${config.bundles.client.webPath}${config.serviceWorker.offlinePageFileName}`,
- offlinePage,
- );
-}
-
-// Configure serving of our client bundle.
-app.use(config.bundles.client.webPath, clientBundle);
-
-// Configure static serving of our "public" root http path static files.
-// Note: these will be served off the root (i.e. '/') of our application.
-app.use(express.static(pathResolve(appRootDir.get(), config.publicAssetsPath)));
-
-// The React application middleware.
-app.get('*', reactApplication);
-
-// Error Handler middlewares.
-app.use(...errorHandlers);
-
-// Create an http listener for our express app.
-const listener = app.listen(config.port, config.host, () =>
- console.log(`Server listening on port ${config.port}`),
-);
-
-// We export the listener as it will be handy for our development hot reloader,
-// or for exposing a general extension layer for application customisations.
-export default listener;
diff --git a/src/server/middleware/clientBundle.js b/src/server/middleware/clientBundle.js
deleted file mode 100644
index 2fc2b8b1..00000000
--- a/src/server/middleware/clientBundle.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import express from 'express';
-import { resolve as pathResolve } from 'path';
-import appRootDir from 'app-root-dir';
-import config from '../../../config';
-
-// Middleware to server our client bundle.
-export default express.static(
- pathResolve(appRootDir.get(), config.bundles.client.outputPath),
- { maxAge: config.browserCacheMaxAge },
-);
diff --git a/src/server/middleware/errorHandlers.js b/src/server/middleware/errorHandlers.js
deleted file mode 100644
index d534ec1d..00000000
--- a/src/server/middleware/errorHandlers.js
+++ /dev/null
@@ -1,26 +0,0 @@
-/* eslint-disable no-console */
-/* eslint-disable no-unused-vars */
-
-const errorHandlersMiddleware = [
- // Handle 404 errors.
- // Note: the react application middleware hands 404 paths, but it is good to
- // have this backup for paths not handled by the universal middleware. For
- // example you may bind a /api path to express.
- function notFoundMiddlware(req, res, next) {
- res.status(404).send('Sorry, that resource was not found.');
- },
-
- // Handle all unhandled errors.
- // Typically you want to return a "500" response status.
- // Note: You must provide specify all 4 parameters on this callback function
- // even if they aren't used, otherwise it won't be used.
- function unhandledErrorMiddleware(err, req, res, next) {
- if (err) {
- console.log(err);
- console.log(err.stack);
- }
- res.status(500).send('Sorry, an unexpected error occurred.');
- },
-];
-
-export default errorHandlersMiddleware;
diff --git a/src/server/middleware/reactApplication/generateHTML.js b/src/server/middleware/reactApplication/generateHTML.js
deleted file mode 100644
index 386a98a0..00000000
--- a/src/server/middleware/reactApplication/generateHTML.js
+++ /dev/null
@@ -1,117 +0,0 @@
-// This module is responsible for generating the HTML page response for
-// the react application middleware.
-//
-// NOTE: If you are using a service worker to support offline mode for your
-// application then please make sure that you keep the structure of the html
-// within this module in sync with the module used to generate the offline
-// HTML page.
-// @see ./tools/webpack/offlinePage/generate.js
-
-import serialize from 'serialize-javascript';
-import { STATE_IDENTIFIER } from 'code-split-component';
-import getAssetsForClientChunks from './getAssetsForClientChunks';
-import config, { clientConfig } from '../../../../config';
-
-function styleTags(styles) {
- return styles
- .map(style =>
- ``,
- )
- .join('\n');
-}
-
-function scriptTag(jsFilePath) {
- return ``;
-}
-
-function scriptTags(jsFilePaths) {
- return jsFilePaths.map(scriptTag).join('\n');
-}
-
-
-export default function generateHTML(args) {
- const { reactAppString, initialState, nonce, helmet, codeSplitState } = args;
-
- // The chunks that we need to fetch the assets (js/css) for and then include
- // said assets as script/style tags within our html.
- const chunksForRender = [
- // We always manually add the main entry chunk for our client bundle. It
- // must always be the first item in the collection.
- 'index',
- ];
-
- if (codeSplitState) {
- // We add all the chunks that our CodeSplitProvider tracked as being used
- // for this render. This isn't actually required as the rehydrate function
- // of code-split-component which gets executed in our client bundle will
- // ensure all our required chunks are loaded, but its a nice optimisation as
- // it means the browser can start fetching the required files before it's
- // even finished parsing our client bundle entry script.
- // Having the assets.json file available to us made implementing this
- // feature rather arbitrary.
- codeSplitState.chunks.forEach(chunk => chunksForRender.push(chunk));
- }
-
- // Now we get the assets (js/css) for the chunks.
- const assetsForRender = getAssetsForClientChunks(chunksForRender);
-
- // Creates an inline script definition that is protected by the nonce.
- const inlineScript = body =>
- ``;
-
- return `
-
-
- ${helmet ? helmet.title.toString() : ''}
- ${helmet ? helmet.meta.toString() : ''}
- ${helmet ? helmet.link.toString() : ''}
- ${styleTags(assetsForRender.css)}
- ${helmet ? helmet.style.toString() : ''}
-
-
-
${reactAppString || ''}
- ${
- // Binds the client configuration object to the window object so
- // that we can safely expose some configuration values to the
- // client bundle that gets executed in the browser.
- inlineScript(`window.__CLIENT_CONFIG__=${serialize(clientConfig)}`)
- }
- ${
- // Bind the initial application state based on the server render
- // so the client can register the correct initial state for the view.
- initialState
- ? inlineScript(`window.__APP_STATE__=${serialize(initialState)};`)
- : ''
- }
- ${
- // Bind our code split state so that the client knows which server
- // rendered modules need to be rehydrated.
- codeSplitState
- ? inlineScript(`window.${STATE_IDENTIFIER}=${serialize(codeSplitState)};`)
- : ''
- }
- ${
- // Enable the polyfill io script?
- // This can't be configured within a react-helmet component as we
- // may need the polyfill's before our client bundle gets parsed.
- config.polyfillIO.enabled
- ? scriptTag(config.polyfillIO.url)
- : ''
- }
- ${
- // When we are in development mode our development server will generate a
- // vendor DLL in order to dramatically reduce our compilation times. Therefore
- // we need to inject the path to the vendor dll bundle below.
- // @see /tools/development/ensureVendorDLLExists.js
- process.env.NODE_ENV === 'development'
- && config.bundles.client.devVendorDLL.enabled
- ? scriptTag(`${config.bundles.client.webPath}${config.bundles.client.devVendorDLL.name}.js?t=${Date.now()}`)
- : ''
- }
- ${scriptTags(assetsForRender.js)}
- ${helmet ? helmet.script.toString() : ''}
-
- `;
-}
diff --git a/src/server/middleware/reactApplication/getAssetsForClientChunks.js b/src/server/middleware/reactApplication/getAssetsForClientChunks.js
deleted file mode 100644
index 285f015b..00000000
--- a/src/server/middleware/reactApplication/getAssetsForClientChunks.js
+++ /dev/null
@@ -1,59 +0,0 @@
-// This file resolves the assets available from our client bundle.
-
-import fs from 'fs';
-import { resolve as pathResolve } from 'path';
-import appRootDir from 'app-root-dir';
-import config from '../../../../config';
-
-const assetsFilePath = pathResolve(
- appRootDir.get(),
- config.bundles.client.outputPath,
- `./${config.bundleAssetsFileName}`,
-);
-
-if (!fs.existsSync(assetsFilePath)) {
- throw new Error(
- `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.`,
- );
-}
-
-const readAssetsJSONFile = () => JSON.parse(fs.readFileSync(assetsFilePath, 'utf8'));
-const assetsJSON = readAssetsJSONFile();
-const assetsJSONResolver = () => (
- process.env.NODE_ENV === 'development'
- // In development mode we always read the assets json file from disk to avoid
- // any cases where an older version gets cached.
- ? readAssetsJSONFile()
- // Otherwise we return the initially parsed JSON file.
- : assetsJSON
-);
-
-/**
- * Retrieves the js/css for the named chunks that belong to our client bundle.
- *
- * Note: the order of the chunk names is important. The same ordering will be
- * used when rendering the scripts.
- *
- * This is useful to us for a couple of reasons:
- * - It allows us to target the assets for a specific chunk, thereby only
- * loading the assets we know we will need for a specific request.
- * - The assets are hashed, and therefore they can't be "manually" added
- * to the render logic. Having this method allows us to easily fetch
- * the respective assets simply by using a chunk name. :)
- */
-function getAssetsForClientChunks(chunks) {
- return chunks.reduce((acc, chunkName) => {
- const chunkAssets = assetsJSONResolver()[chunkName];
- if (chunkAssets) {
- if (chunkAssets.js) {
- acc.js.push(chunkAssets.js);
- }
- if (chunkAssets.css) {
- acc.css.push(chunkAssets.css);
- }
- }
- return acc;
- }, { js: [], css: [] });
-}
-
-export default getAssetsForClientChunks;
diff --git a/src/server/middleware/reactApplication/index.js b/src/server/middleware/reactApplication/index.js
deleted file mode 100644
index ba244bc9..00000000
--- a/src/server/middleware/reactApplication/index.js
+++ /dev/null
@@ -1,96 +0,0 @@
-
-import React from 'react';
-import { renderToString } from 'react-dom/server';
-import { ServerRouter, createServerRenderContext } from 'react-router';
-import { CodeSplitProvider, createRenderContext } from 'code-split-component';
-import Helmet from 'react-helmet';
-import generateHTML from './generateHTML';
-import DemoApp from '../../../shared/components/DemoApp';
-import config from '../../../../config';
-
-/**
- * An express middleware that is capabable of service our React application,
- * supporting server side rendering of the application.
- */
-function reactApplicationMiddleware(request, response) {
- // We should have had a nonce provided to us. See the server/index.js for
- // more information on what this is.
- if (typeof response.locals.nonce !== 'string') {
- throw new Error('A "nonce" value has not been attached to the response');
- }
- const nonce = response.locals.nonce;
-
- // It's possible to disable SSR, which can be useful in development mode.
- // In this case traditional client side only rendering will occur.
- if (config.disableSSR) {
- if (process.env.NODE_ENV === 'development') {
- // eslint-disable-next-line no-console
- console.log('==> Handling react route without SSR');
- }
- // SSR is disabled so we will just return an empty html page and will
- // rely on the client to initialize and render the react application.
- const html = generateHTML({
- // Nonce which allows us to safely declare inline scripts.
- nonce,
- });
- response.status(200).send(html);
- return;
- }
-
- // First create a context for , which will allow us to
- // query for the results of the render.
- const reactRouterContext = createServerRenderContext();
-
- // We also create a context for our which will allow us
- // to query which chunks/modules were used during the render process.
- const codeSplitContext = createRenderContext();
-
- // Create our React application and render it into a string.
- const reactAppString = renderToString(
-
-
-
-
- ,
- );
-
- // Generate the html response.
- const html = generateHTML({
- // Provide the full app react element.
- reactAppString,
- // Nonce which allows us to safely declare inline scripts.
- nonce,
- // Running this gets all the helmet properties (e.g. headers/scripts/title etc)
- // that need to be included within our html. It's based on the rendered app.
- // @see https://github.com/nfl/react-helmet
- helmet: Helmet.rewind(),
- // We provide our code split state so that it can be included within the
- // html, and then the client bundle can use this data to know which chunks/
- // modules need to be rehydrated prior to the application being rendered.
- codeSplitState: codeSplitContext.getState(),
- });
-
- // Get the render result from the server render context.
- const renderResult = reactRouterContext.getResult();
-
- // Check if the render result contains a redirect, if so we need to set
- // the specific status and redirect header and end the response.
- if (renderResult.redirect) {
- response.status(301).setHeader('Location', renderResult.redirect.pathname);
- response.end();
- return;
- }
-
- response
- .status(
- renderResult.missed
- // If the renderResult contains a "missed" match then we set a 404 code.
- // Our App component will handle the rendering of an Error404 view.
- ? 404
- // Otherwise everything is all good and we send a 200 OK status.
- : 200,
- )
- .send(html);
-}
-
-export default (reactApplicationMiddleware);
diff --git a/src/shared/README.md b/src/shared/README.md
deleted file mode 100644
index c0977efa..00000000
--- a/src/shared/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# src/shared
-
-This directory should contain code that is considered safe to execute on a `node` or `web` target bundle. i.e. It's "Universal" code. :)
-
-This means all our bundles can use this code.
diff --git a/src/shared/components/DemoApp/About/About.js b/src/shared/components/DemoApp/About/About.js
deleted file mode 100644
index e93e8398..00000000
--- a/src/shared/components/DemoApp/About/About.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from 'react';
-import Helmet from 'react-helmet';
-import Contributor from './Contributor';
-
-const contributors = [
- { name: 'Alin Porumb', twitter: 'alinporumb' },
- { name: 'Benjamin Kniffler', twitter: 'bkniffler' },
- { name: 'Carson Perrotti', twitter: 'carsonp' },
- { name: 'Christian Glombek', twitter: 'LorbusChris' },
- { name: 'Christoph Werner', twitter: 'code_punkt' },
- // TODO: Get David's twitter handle.
- // { name: 'David Edmondson', twitter: '' },
- // TODO: Get Evgeny's twitter handle.
- // { name: 'Evgeny Boxer', twitter: '' },
- { name: 'Joe Kohlmann', twitter: 'jkohlmann' },
- { name: 'Lucian Lature', twitter: 'lucianlature' },
- { name: 'Steven Enten', twitter: 'steven_enten' },
- { name: 'Sean Matheson', twitter: 'controlplusb' },
- { name: 'Steven Truesdell', twitter: 'StruesCO' },
-];
-
-function About() {
- return (
-