From 9ee484c51d5c34e9b85ad6cb38293a5cdafcb57c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9rgio=20A=2E=20Kopplin?=
Date: Wed, 21 Jun 2017 16:55:33 -0700
Subject: [PATCH] next -> redux opinionated (#366)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Adds modernizr with illustrative example. closes #256
* Linting now occurs on config, src and tools. closes #314
* Updates dependencies.
* update broken feature/flow link in docs
* Updates dependencies.
* Updated CSP to add backwards compatibility to nonce. (#344)
* Updated CSP for nonce backwards compatibility
* Documented
* Updates comments
* - Renamed environment variables:
- `SERVER_PORT` to `PORT`
- `SERVER_HOST` to `HOST`
- `CLIENT_DEVSERVER_PORT` to `CLIENT_DEV_PORT`
- Replaces `code-split-component` with `react-async-component`
- 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 how we resolve environment specific configuration values. `NODE_ENV` is reserved for specifying a `development` or `production` build now. Use `CONF_ENV` to specify a target environment if you would like to resolve an environment specific .env file.
- Refactors the client configuration filter rule to be contained within the main configuration and moves the configuration object creation into the server middleware.
- Renames the `safeGetConfig` to `config`, and made it a default export.
- Removes the `cross-env` library.
- All server/client/shared code all use the shared config helper.
- Updated dependencies, including to the latest Webpack official 2 release.
- New babel plugins to optimise React production build performance.
- Adds new icon sets.
- Chrome favicon request issue.
- Cleans up the package scripts.
- Service worker would fail if a subfolder was added to the public folder.
* fix sw to exclude directory (#305)
* Adds Dion Dirza as a contributor ๐
* Fixes analyze.
* Docs and cleaning up.
* Updates docs and version.
* Removes code-split-component.
* Updates deps and fixes config refs.
* Huuuuuge refactor of project structure. The 'src/' was always unnecessary. Everything internal (tooling/docs) has been moved to an "internal" folder. The rest lives at root.
The configuration has been given a massive once over too. I really disliked how there were multiple ways the config values were being read throughout the project. Everything has been changed now to use a config helper that lives in the '/config/' folder. All things config lives in that folder now too.
* Fixes offline page.
* Cleans up and centralises util functions.
* Minor fixes.
* Renames config/get to config/getConfig
Removes CONF_ENV and refactors environment variable resolution to use NODE_ENV again.
* fixes typo: laoder -> loader (#353)
* fixes typo: server -> serve (#354)
* Huuuuuuuge update. Getting closer to v13.
* Fixes incorrect import path in Menu.test.js (#360)
* Updates Error 404 component so that we can set 404 status on SSR renders (#357)
* Updates dependencies.
* Updates contributors and comments.
* Restructures and fixes tests.
* Cleaning up comments and docs.
* Fixes deprecated fallbackLoader (#365)
* Changes default host config to bind to all hosts (0.0.0.0) by default (#361)
* fix typo flag (#369)
* removing flow from branch
* Missing default polyfill.io features
* Updated react-router-dom to beta 7 (#381)
* Updates node version.
Adds check for none-support of HTML 5 browserhistory on the router.
* Synchronises the react-router-dom dep into yarn.lock
* Updates dependencies.
* Removes onlyIf and replaces with ifElse helper. fixes #362
* Update README.md
* Update README.md
* Fixes breaking context changes in RR4
* Rollback disableSSR. TODO: EnvVars this
* Changes polyfill.io into a features array specification.
* Upgrade react-router-dom@4.0.0 and other deps (#396)
* update deps
* Using babel-env instead, still produce same result
* Fix not found implementation for stable react-router v4.0.0
* Restore and update deps
* Syncs yarn.lock with package.json
* Fix Error404 test
* Upgrades to latest react-async-component.
Reintroduces React Hot Loader ๐
Adds milligram for some basic styling out of the box.
Adds a counter route for testing state based hot reloading.
Restructures route components.
* Adds 'es6' features to polyfill.io config.
* Removes aliasing of React libs and instead makes NODE_ENV get set to either 'development' or 'production' via the webpack config.
Introduces DEPLOYMENT environment variable to allow for the specification of target environment configuration files.
* Updates comments.
* Removes babel-preset-latest.
* staticContext on 404 route not required.
* Refactors async components structure.
* Adds prettier ๐
* Updates nvm
* Adds prepush hook to run jest.
* Fixes prettier config to use prettier-eslint. Woops\!
* Removes unused eslint-flowtype plugin.
* Updates dependencies.
* Updates docs
* Moves all html page elements to DemoApp component. closes #390
* Removes 'host' from server start. closes #398
* Changes process.env build flags to be inline with expected string type of typical process.env member. closes #395
* Preps v13 release
* Update README.md
* Update README.md
* Updates dependencies.
* Update breaking changes
* Adds birkir to contributors list
* Adds @birkir to contributors on about us route.
* Changes over to all-contributors
* Orders contributors by name
* fix the "collectCoverageFrom" config of jest
* updating asyncPosts
* Prepare React 15.5 (#413)
* Update dependecies, change component prop-types.
* Helmet now has prop-types support.
* Update yarn lockfile
* Bumped react-router and forgot prop types in clientconfig.
* Bump enzyme and prop-types
* Bump react-async-component and enzyme
* Updates deps.
* Another bug fixes, tweaks (#422)
* Fix lint
* code split naming strategy sample
* remove HTTP protocol to be more flexible
* Refactor polyfill.io
Due to counter route become broken in mobile device, use only default feature instead
* Fix lint while prettier revert changes
* update deps
* Add es6 back by default. (#429)
* Windows (#415)
* update eslintrc line-breaks for windows
* add prop-types as a dependency to prevent react-hot-loader from breaking build.
add cross-env for starting tasks which break on Windows.
add FAQ for when running develop and javascript files aren't loading even though the build is successful.
modify the clean task to execute a function rather than use the ${npm bin} exec command, which doesnst work on Windows.
* save exact package.json
* add cross-env to npm scripts
* dev server host always 0.0.0.0, not host config
* merge with universally/master - updating snapshots
* merge/next - adding async posts
* merge/next - updating dependencies + updating async components + removing counter component
* merge/next - updating server side state
* merge/next - fixing server async functions
* universally/merge - fixing async posts route
* chore: add redux, react-redux, redux-thunk to vendorDLL
* Fix configuration for extracted style hash (#438)
* Fix broken link to Feature Branches docs. (#419)
* Fix CORS host (#432)
* Removes reference to deprecated react PropTypes import and replaces with the prop-types package. Adds tests for Post route, post reducers and additional tests to home and about route.
* update snapshot
* Fix Router v2 Implementation
* Add color support for stdout (#443)
Passing colors from server to console in develop.
* master -> redux: fixing store problem
* fixing stores
---
.all-contributorsrc | 224 +
.env_example | 6 +-
.eslintignore | 2 -
.eslintrc | 17 +-
.flowconfig | 35 -
.gitignore | 9 +-
.modernizrrc | 7 +
.nvmrc | 2 +-
CHANGELOG.md | 41 +
LICENSE | 2 +-
README.md | 45 +-
client/components/ReactHotLoader.js | 12 +
client/index.js | 86 +
client/polyfills/index.js | 18 +
.../registerServiceWorker.js | 23 +-
config/components/ClientConfig.js | 42 +
config/index.js | 506 +--
config/internals/environmentVars.js | 64 -
config/internals/filterObject.js | 6 +-
config/utils/envVars.js | 80 +
config/values.js | 335 ++
docs/APPLICATION_CONFIG.md | 156 -
docs/FEATURE_FLOW.md | 23 -
docs/FEATURE_REDUX_OPINIONATED.md | 5 -
{tools => internal}/.eslintrc | 0
.../development/createVendorDLL.js | 23 +-
.../development/hotClientServer.js | 16 +-
.../development/hotDevelopment.js | 81 +-
.../development/hotNodeServer.js | 16 +-
{tools => internal}/development/index.js | 7 +-
.../development/listenerManager.js | 9 +-
internal/docs/ADDING_AN_API_BUNDLE.md | 7 +
{docs => internal/docs}/DEPLOY_TO_NOW.md | 18 +-
{docs => internal/docs}/FAQ.md | 49 +-
{docs => internal/docs}/FEATURE_BRANCHES.md | 20 +-
{docs => internal/docs}/PKG_SCRIPTS.md | 36 +-
internal/docs/PROJECT_CONFIG.md | 78 +
{docs => internal/docs}/PROJECT_OVERVIEW.md | 30 +-
internal/jest/assetMock.js | 1 +
{tools => internal}/jest/styleMock.js | 2 +-
internal/scripts/analyze.js | 47 +
internal/scripts/build.js | 34 +
internal/scripts/clean.js | 16 +
{tools => internal}/scripts/deploy.js | 8 +-
{tools => internal}/scripts/preinstall.js | 14 +-
internal/utils.js | 41 +
internal/webpack/configFactory.js | 521 +++
internal/webpack/withServiceWorker/index.js | 143 +
.../withServiceWorker/offlinePageTemplate.js | 20 +
package.json | 178 +-
public/favicon-16x16.png | Bin 1262 -> 0 bytes
public/favicon-32x32.png | Bin 2236 -> 0 bytes
public/favicon.ico | Bin 15086 -> 34494 bytes
.../{ => favicons}/android-chrome-192x192.png | Bin
.../{ => favicons}/android-chrome-512x512.png | Bin
public/favicons/apple-touch-icon-114x114.png | Bin 0 -> 14833 bytes
public/favicons/apple-touch-icon-120x120.png | Bin 0 -> 12787 bytes
public/favicons/apple-touch-icon-144x144.png | Bin 0 -> 21077 bytes
public/favicons/apple-touch-icon-152x152.png | Bin 0 -> 22673 bytes
.../apple-touch-icon-180x180.png} | Bin
public/favicons/apple-touch-icon-57x57.png | Bin 0 -> 5139 bytes
public/favicons/apple-touch-icon-60x60.png | Bin 0 -> 4935 bytes
public/favicons/apple-touch-icon-72x72.png | Bin 0 -> 7046 bytes
public/favicons/apple-touch-icon-76x76.png | Bin 0 -> 7791 bytes
public/favicons/favicon-128.png | Bin 0 -> 11405 bytes
public/favicons/favicon-16x16.png | Bin 0 -> 778 bytes
public/favicons/favicon-196x196.png | Bin 0 -> 35117 bytes
public/favicons/favicon-32x32.png | Bin 0 -> 2000 bytes
public/favicons/favicon-96x96.png | Bin 0 -> 10868 bytes
public/favicons/mstile-144x144.png | Bin 0 -> 21077 bytes
public/favicons/mstile-150x150.png | Bin 0 -> 46449 bytes
public/favicons/mstile-310x150.png | Bin 0 -> 54578 bytes
public/favicons/mstile-310x310.png | Bin 0 -> 128006 bytes
public/favicons/mstile-70x70.png | Bin 0 -> 11405 bytes
public/{ => favicons}/safari-pinned-tab.svg | 0
public/mstile-150x150.png | Bin 11272 -> 0 bytes
{src/server => server}/index.js | 27 +-
server/middleware/clientBundle.js | 11 +
server/middleware/errorHandlers.js | 31 +
.../middleware/offlinePage.js | 27 +-
.../middleware/reactApplication/ServerHTML.js | 140 +
.../getClientBundleEntryAssets.js | 52 +
server/middleware/reactApplication/index.js | 94 +
{src/server => server}/middleware/security.js | 31 +-
server/middleware/serviceWorker.js | 18 +
shared/README.md | 3 +
{src/shared => shared}/actions/posts.js | 10 +-
.../DemoApp/AsyncAboutRoute/AboutRoute.js | 24 +
.../__tests__/AboutRoute.test.js | 17 +
.../__snapshots__/AboutRoute.test.js.snap | 33 +
.../DemoApp/AsyncAboutRoute/index.js | 6 +
.../DemoApp/AsyncHomeRoute/HomeRoute.js | 19 +-
.../__tests__/HomeRoute.test.js | 21 +
.../__snapshots__/HomeRoute.test.js.snap | 15 +-
.../DemoApp/AsyncHomeRoute/index.js | 6 +
.../DemoApp/AsyncPostsRoute}/Post/Post.js | 46 +-
.../Post/__tests__/Post.test.js | 22 +
.../__tests__/__snapshots__/Post.test.js.snap | 16 +
.../DemoApp/AsyncPostsRoute/Post/index.js | 7 +
.../DemoApp/AsyncPostsRoute/index.js | 18 +
.../Error404/__tests__}/Error404.test.js | 8 +-
.../__snapshots__/Error404.test.js.snap | 2 +
shared/components/DemoApp/Error404/index.js | 26 +
.../components/DemoApp/Header/Logo/index.js | 6 +-
.../components/DemoApp/Header/Logo/logo.png | Bin
.../Header/Menu/__tests__}/Menu.test.js | 4 +-
.../__tests__/__snapshots__/Menu.test.js.snap | 38 +
.../components/DemoApp/Header/Menu/index.js | 6 +-
.../components/DemoApp/Header/index.js | 4 +-
.../components/DemoApp/globals.css | 0
shared/components/DemoApp/index.js | 127 +
shared/components/HTML/index.js | 43 +
{src/shared => shared}/reducers/index.js | 17 +-
shared/reducers/posts/__tests__/all.test.js | 7 +
shared/reducers/posts/__tests__/byId.test.js | 7 +
shared/reducers/posts/__tests__/index.test.js | 10 +
{src/shared => shared}/reducers/posts/all.js | 16 +-
{src/shared => shared}/reducers/posts/byId.js | 17 +-
.../shared => shared}/reducers/posts/index.js | 21 +-
.../shared => shared}/redux/configureStore.js | 5 +-
shared/utils/arrays/index.js | 5 +
shared/utils/arrays/removeNil.js | 10 +
shared/utils/logic/ifElse.js | 21 +
shared/utils/logic/index.js | 5 +
shared/utils/objects/filterWithRules.js | 56 +
shared/utils/objects/index.js | 4 +
shared/utils/objects/mergeDeep.js | 35 +
src/client/components/ReactHotLoader.js | 15 -
src/client/index.js | 69 -
src/server/middleware/clientBundle.js | 12 -
src/server/middleware/errorHandlers.js | 30 -
.../reactApplication/generateHTML.js | 133 -
.../getAssetsForClientChunks.js | 61 -
.../middleware/reactApplication/index.js | 131 -
src/server/middleware/serviceWorker.js | 21 -
src/shared/README.md | 5 -
src/shared/components/DemoApp/About/About.js | 44 -
.../components/DemoApp/About/About.test.js | 13 -
.../DemoApp/About/Contributor/Contributor.js | 20 -
.../DemoApp/About/Contributor/index.js | 1 -
.../About/__snapshots__/About.test.js.snap | 18 -
src/shared/components/DemoApp/About/index.js | 3 -
src/shared/components/DemoApp/DemoApp.js | 64 -
.../components/DemoApp/Error404/Error404.js | 11 -
.../components/DemoApp/Error404/index.js | 3 -
.../components/DemoApp/Header/Logo/index.js | 3 -
.../Menu/__snapshots__/Menu.test.js.snap | 35 -
.../components/DemoApp/Header/Menu/index.js | 3 -
src/shared/components/DemoApp/Header/index.js | 3 -
.../components/DemoApp/Home/Home.test.js | 13 -
src/shared/components/DemoApp/Home/index.js | 3 -
.../components/DemoApp/Posts/Post/index.js | 3 -
src/shared/components/DemoApp/Posts/Posts.js | 25 -
src/shared/components/DemoApp/Posts/index.js | 3 -
src/shared/components/DemoApp/index.js | 3 -
src/shared/types/model.js | 8 -
src/shared/types/react-router.js | 10 -
src/shared/types/react.js | 15 -
src/shared/types/redux.js | 18 -
src/shared/utils/config.js | 88 -
tools/flow/stubs/WebpackAsset.js.flow | 3 -
tools/flow/typeExtensions/commonjs.js | 18 -
tools/flow/typeExtensions/es6modules.js | 5 -
tools/jest/assetMock.js | 1 -
tools/scripts/analyze.js | 37 -
tools/scripts/build.js | 31 -
tools/scripts/clean.js | 12 -
tools/scripts/flow.js | 22 -
tools/types.js | 4 -
tools/utils.js | 123 -
tools/webpack/client.config.babel.js | 11 -
tools/webpack/configFactory.js | 640 ---
tools/webpack/offlinePage/generate.js | 64 -
tools/webpack/offlinePage/index.js | 7 -
tools/webpack/server.config.babel.js | 10 -
yarn.lock | 3843 +++++++++--------
176 files changed, 5302 insertions(+), 4771 deletions(-)
create mode 100644 .all-contributorsrc
delete mode 100644 .flowconfig
create mode 100644 .modernizrrc
create mode 100644 client/components/ReactHotLoader.js
create mode 100644 client/index.js
create mode 100644 client/polyfills/index.js
rename {src/client => client}/registerServiceWorker.js (65%)
create mode 100644 config/components/ClientConfig.js
delete mode 100644 config/internals/environmentVars.js
create mode 100644 config/utils/envVars.js
create mode 100644 config/values.js
delete mode 100644 docs/APPLICATION_CONFIG.md
delete mode 100644 docs/FEATURE_FLOW.md
delete mode 100644 docs/FEATURE_REDUX_OPINIONATED.md
rename {tools => internal}/.eslintrc (100%)
rename {tools => internal}/development/createVendorDLL.js (87%)
rename {tools => internal}/development/hotClientServer.js (87%)
rename {tools => internal}/development/hotDevelopment.js (57%)
rename {tools => internal}/development/hotNodeServer.js (90%)
rename {tools => internal}/development/index.js (92%)
rename {tools => internal}/development/listenerManager.js (89%)
create mode 100644 internal/docs/ADDING_AN_API_BUNDLE.md
rename {docs => internal/docs}/DEPLOY_TO_NOW.md (60%)
rename {docs => internal/docs}/FAQ.md (65%)
rename {docs => internal/docs}/FEATURE_BRANCHES.md (70%)
rename {docs => internal/docs}/PKG_SCRIPTS.md (56%)
create mode 100644 internal/docs/PROJECT_CONFIG.md
rename {docs => internal/docs}/PROJECT_OVERVIEW.md (61%)
create mode 100644 internal/jest/assetMock.js
rename {tools => internal}/jest/styleMock.js (74%)
create mode 100644 internal/scripts/analyze.js
create mode 100644 internal/scripts/build.js
create mode 100644 internal/scripts/clean.js
rename {tools => internal}/scripts/deploy.js (51%)
rename {tools => internal}/scripts/preinstall.js (83%)
create mode 100644 internal/utils.js
create mode 100644 internal/webpack/configFactory.js
create mode 100644 internal/webpack/withServiceWorker/index.js
create mode 100644 internal/webpack/withServiceWorker/offlinePageTemplate.js
delete mode 100644 public/favicon-16x16.png
delete mode 100644 public/favicon-32x32.png
rename public/{ => favicons}/android-chrome-192x192.png (100%)
rename public/{ => favicons}/android-chrome-512x512.png (100%)
create mode 100644 public/favicons/apple-touch-icon-114x114.png
create mode 100644 public/favicons/apple-touch-icon-120x120.png
create mode 100644 public/favicons/apple-touch-icon-144x144.png
create mode 100644 public/favicons/apple-touch-icon-152x152.png
rename public/{apple-touch-icon.png => favicons/apple-touch-icon-180x180.png} (100%)
create mode 100644 public/favicons/apple-touch-icon-57x57.png
create mode 100644 public/favicons/apple-touch-icon-60x60.png
create mode 100644 public/favicons/apple-touch-icon-72x72.png
create mode 100644 public/favicons/apple-touch-icon-76x76.png
create mode 100644 public/favicons/favicon-128.png
create mode 100644 public/favicons/favicon-16x16.png
create mode 100644 public/favicons/favicon-196x196.png
create mode 100644 public/favicons/favicon-32x32.png
create mode 100644 public/favicons/favicon-96x96.png
create mode 100644 public/favicons/mstile-144x144.png
create mode 100644 public/favicons/mstile-150x150.png
create mode 100644 public/favicons/mstile-310x150.png
create mode 100644 public/favicons/mstile-310x310.png
create mode 100644 public/favicons/mstile-70x70.png
rename public/{ => favicons}/safari-pinned-tab.svg (100%)
delete mode 100644 public/mstile-150x150.png
rename {src/server => server}/index.js (62%)
create mode 100644 server/middleware/clientBundle.js
create mode 100644 server/middleware/errorHandlers.js
rename {src/server => server}/middleware/offlinePage.js (51%)
create mode 100644 server/middleware/reactApplication/ServerHTML.js
create mode 100644 server/middleware/reactApplication/getClientBundleEntryAssets.js
create mode 100644 server/middleware/reactApplication/index.js
rename {src/server => server}/middleware/security.js (83%)
create mode 100644 server/middleware/serviceWorker.js
create mode 100644 shared/README.md
rename {src/shared => shared}/actions/posts.js (77%)
create mode 100644 shared/components/DemoApp/AsyncAboutRoute/AboutRoute.js
create mode 100644 shared/components/DemoApp/AsyncAboutRoute/__tests__/AboutRoute.test.js
create mode 100644 shared/components/DemoApp/AsyncAboutRoute/__tests__/__snapshots__/AboutRoute.test.js.snap
create mode 100644 shared/components/DemoApp/AsyncAboutRoute/index.js
rename src/shared/components/DemoApp/Home/Home.js => shared/components/DemoApp/AsyncHomeRoute/HomeRoute.js (63%)
create mode 100644 shared/components/DemoApp/AsyncHomeRoute/__tests__/HomeRoute.test.js
rename src/shared/components/DemoApp/Home/__snapshots__/Home.test.js.snap => shared/components/DemoApp/AsyncHomeRoute/__tests__/__snapshots__/HomeRoute.test.js.snap (61%)
create mode 100644 shared/components/DemoApp/AsyncHomeRoute/index.js
rename {src/shared/components/DemoApp/Posts => shared/components/DemoApp/AsyncPostsRoute}/Post/Post.js (77%)
create mode 100644 shared/components/DemoApp/AsyncPostsRoute/Post/__tests__/Post.test.js
create mode 100644 shared/components/DemoApp/AsyncPostsRoute/Post/__tests__/__snapshots__/Post.test.js.snap
create mode 100644 shared/components/DemoApp/AsyncPostsRoute/Post/index.js
create mode 100644 shared/components/DemoApp/AsyncPostsRoute/index.js
rename {src/shared/components/DemoApp/Error404 => shared/components/DemoApp/Error404/__tests__}/Error404.test.js (55%)
rename {src/shared/components/DemoApp/Error404 => shared/components/DemoApp/Error404/__tests__}/__snapshots__/Error404.test.js.snap (66%)
create mode 100644 shared/components/DemoApp/Error404/index.js
rename src/shared/components/DemoApp/Header/Logo/Logo.js => shared/components/DemoApp/Header/Logo/index.js (52%)
rename {src/shared => shared}/components/DemoApp/Header/Logo/logo.png (100%)
rename {src/shared/components/DemoApp/Header/Menu => shared/components/DemoApp/Header/Menu/__tests__}/Menu.test.js (87%)
create mode 100644 shared/components/DemoApp/Header/Menu/__tests__/__snapshots__/Menu.test.js.snap
rename src/shared/components/DemoApp/Header/Menu/Menu.js => shared/components/DemoApp/Header/Menu/index.js (64%)
rename src/shared/components/DemoApp/Header/Header.js => shared/components/DemoApp/Header/index.js (73%)
rename {src/shared => shared}/components/DemoApp/globals.css (100%)
create mode 100644 shared/components/DemoApp/index.js
create mode 100644 shared/components/HTML/index.js
rename {src/shared => shared}/reducers/index.js (52%)
create mode 100644 shared/reducers/posts/__tests__/all.test.js
create mode 100644 shared/reducers/posts/__tests__/byId.test.js
create mode 100644 shared/reducers/posts/__tests__/index.test.js
rename {src/shared => shared}/reducers/posts/all.js (60%)
rename {src/shared => shared}/reducers/posts/byId.js (55%)
rename {src/shared => shared}/reducers/posts/index.js (52%)
rename {src/shared => shared}/redux/configureStore.js (96%)
create mode 100644 shared/utils/arrays/index.js
create mode 100644 shared/utils/arrays/removeNil.js
create mode 100644 shared/utils/logic/ifElse.js
create mode 100644 shared/utils/logic/index.js
create mode 100644 shared/utils/objects/filterWithRules.js
create mode 100644 shared/utils/objects/index.js
create mode 100644 shared/utils/objects/mergeDeep.js
delete mode 100644 src/client/components/ReactHotLoader.js
delete mode 100644 src/client/index.js
delete mode 100644 src/server/middleware/clientBundle.js
delete mode 100644 src/server/middleware/errorHandlers.js
delete mode 100644 src/server/middleware/reactApplication/generateHTML.js
delete mode 100644 src/server/middleware/reactApplication/getAssetsForClientChunks.js
delete mode 100644 src/server/middleware/reactApplication/index.js
delete mode 100644 src/server/middleware/serviceWorker.js
delete mode 100644 src/shared/README.md
delete mode 100644 src/shared/components/DemoApp/About/About.js
delete mode 100644 src/shared/components/DemoApp/About/About.test.js
delete mode 100644 src/shared/components/DemoApp/About/Contributor/Contributor.js
delete mode 100644 src/shared/components/DemoApp/About/Contributor/index.js
delete mode 100644 src/shared/components/DemoApp/About/__snapshots__/About.test.js.snap
delete mode 100644 src/shared/components/DemoApp/About/index.js
delete mode 100644 src/shared/components/DemoApp/DemoApp.js
delete mode 100644 src/shared/components/DemoApp/Error404/Error404.js
delete mode 100644 src/shared/components/DemoApp/Error404/index.js
delete mode 100644 src/shared/components/DemoApp/Header/Logo/index.js
delete mode 100644 src/shared/components/DemoApp/Header/Menu/__snapshots__/Menu.test.js.snap
delete mode 100644 src/shared/components/DemoApp/Header/Menu/index.js
delete mode 100644 src/shared/components/DemoApp/Header/index.js
delete mode 100644 src/shared/components/DemoApp/Home/Home.test.js
delete mode 100644 src/shared/components/DemoApp/Home/index.js
delete mode 100644 src/shared/components/DemoApp/Posts/Post/index.js
delete mode 100644 src/shared/components/DemoApp/Posts/Posts.js
delete mode 100644 src/shared/components/DemoApp/Posts/index.js
delete mode 100644 src/shared/components/DemoApp/index.js
delete mode 100644 src/shared/types/model.js
delete mode 100644 src/shared/types/react-router.js
delete mode 100644 src/shared/types/react.js
delete mode 100644 src/shared/types/redux.js
delete mode 100644 src/shared/utils/config.js
delete mode 100644 tools/flow/stubs/WebpackAsset.js.flow
delete mode 100644 tools/flow/typeExtensions/commonjs.js
delete mode 100644 tools/flow/typeExtensions/es6modules.js
delete mode 100644 tools/jest/assetMock.js
delete mode 100644 tools/scripts/analyze.js
delete mode 100644 tools/scripts/build.js
delete mode 100644 tools/scripts/clean.js
delete mode 100644 tools/scripts/flow.js
delete mode 100644 tools/types.js
delete mode 100644 tools/utils.js
delete mode 100644 tools/webpack/client.config.babel.js
delete mode 100644 tools/webpack/configFactory.js
delete mode 100644 tools/webpack/offlinePage/generate.js
delete mode 100644 tools/webpack/offlinePage/index.js
delete mode 100644 tools/webpack/server.config.babel.js
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..22018d01 100644
--- a/.env_example
+++ b/.env_example
@@ -9,11 +9,11 @@
# ==============================================================================
# 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
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 c8153999..40290cd2 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,9 +1,6 @@
{
"parser": "babel-eslint",
"extends": "airbnb",
- "plugins": [
- "flowtype"
- ],
"env": {
"browser": true,
"es6": true,
@@ -14,15 +11,13 @@
"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,
- // This rule struggles with flow.
- "react/prop-types": 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
}
}
diff --git a/.flowconfig b/.flowconfig
deleted file mode 100644
index f98531f0..00000000
--- a/.flowconfig
+++ /dev/null
@@ -1,35 +0,0 @@
-[include]
-
-[ignore]
-
-# Including these files causes issues.
-.*/node_modules/fbjs/.*
-.*/node_modules/flow-remove-types/.*
-.*/node_modules/flow-coverage-report/.*
-
-[libs]
-
-# Official "flow-typed" repository definitions.
-flow-typed/npm
-
-# Type extensions.
-tools/flow/typeExtensions/
-
-# Note: the following definitions come bundled with flow. It can be handy
-# to reference them.
-# React: https://github.com/facebook/flow/blob/master/lib/react.js
-# Javascript: https://github.com/facebook/flow/blob/master/lib/core.js
-# Node: https://github.com/facebook/flow/blob/master/lib/node.js
-# DOM: https://github.com/facebook/flow/blob/master/lib/dom.js
-# BOM: https://github.com/facebook/flow/blob/master/lib/bom.js
-# CSSOM: https://github.com/facebook/flow/blob/master/lib/cssom.js
-# IndexDB: https://github.com/facebook/flow/blob/master/lib/indexeddb.js
-
-[options]
-
-# This is so that we can import static files in our webpack supported components
-# and not have flow throw a hissy fit.
-module.name_mapper='^\(.*\)\.\(css\|eot\|gif\|ico\|jpg\|jpeg\|less\|otf\|mp3\|mp4\|ogg\|png\|sass\|scss\|sss\|svg\|swf\|ttf\|webp\|woff\|woff2\)$' -> '/tools/flow/stubs/WebpackAsset.js.flow'
-
-[version]
-0.37.4
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..918af42d 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-v6.9.2
+v6.10.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 5a550eb9..9d35e0a5 100644
--- a/README.md
+++ b/README.md
@@ -7,27 +7,32 @@ Note: This is a feature branch of `react-universally`. Please see the [`FEATURE_
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.
@@ -35,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
@@ -48,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/):
@@ -57,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/client/components/ReactHotLoader.js b/client/components/ReactHotLoader.js
new file mode 100644
index 00000000..18e53476
--- /dev/null
+++ b/client/components/ReactHotLoader.js
@@ -0,0 +1,12 @@
+/* eslint-disable global-require */
+/* eslint-disable import/no-extraneous-dependencies */
+
+import React from 'react';
+
+// 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'
+ ? require('react-hot-loader').AppContainer
+ : ({ children }) => React.Children.only(children);
+
+export default ReactHotLoader;
diff --git a/client/index.js b/client/index.js
new file mode 100644
index 00000000..0d0375a9
--- /dev/null
+++ b/client/index.js
@@ -0,0 +1,86 @@
+/* eslint-disable global-require */
+
+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 { JobProvider } from 'react-jobs';
+import { Provider } from 'react-redux';
+import configureStore from '../shared/redux/configureStore';
+
+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');
+
+// Create our Redux store.
+const store = configureStore(
+ // Server side rendering would have mounted our state on this global.
+ window.__APP_STATE__, // eslint-disable-line no-underscore-dangle
+);
+
+// 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__;
+
+// Get any "rehydrate" state sent back by the server
+// eslint-disable-next-line no-underscore-dangle
+const rehydrateState = window.__JOBS_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 bc356edb..660c7381 100644
--- a/config/index.js
+++ b/config/index.js
@@ -1,397 +1,115 @@
-/* @flow */
-
-// 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';
-import type { BuildOptions } from '../tools/types';
-
-// 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 : Object, buildOptions : 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 : Object, buildOptions : 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 b8345da8..00000000
--- a/config/internals/environmentVars.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/* @flow */
-
-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 : string, defaultVal : string) {
- return process.env[name] || defaultVal;
-}
-
-export function getIntEnvVar(name : string, defaultVal : number) {
- return process.env[name]
- ? parseInt(process.env[name], 10)
- : defaultVal;
-}
-
-export function getBoolVar(name : string, defaultVal : boolean) {
- return process.env[name]
- ? process.env[name] === 'true'
- : defaultVal;
-}
diff --git a/config/internals/filterObject.js b/config/internals/filterObject.js
index 6c5e9e67..d821aef1 100644
--- a/config/internals/filterObject.js
+++ b/config/internals/filterObject.js
@@ -1,6 +1,4 @@
-/* @flow */
-
-function filterObjectLoop(obj : Object, filters : Object, basePropPath = '') : Object {
+function filterObjectLoop(obj, filters, basePropPath = '') {
return Object.keys(filters).reduce((acc, key) => {
const propPath = basePropPath !== '' ? `${basePropPath}.${key}` : key;
@@ -46,6 +44,6 @@ function filterObjectLoop(obj : Object, filters : Object, basePropPath = '') : O
// foo: { bar: 'bar' },
// poop: { plop: 'splash' }
// },
-export default function filterObject(obj : Object, filters : Object) : Object {
+export default function filterObject(obj, filters) {
return filterObjectLoop(obj, filters);
}
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..3f30ebe5
--- /dev/null
+++ b/config/values.js
@@ -0,0 +1,335 @@
+/**
+ * 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,
+ // 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),
+
+ // 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',
+ ],
+ styleSrc: [
+ 'cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css',
+ 'fonts.googleapis.com/css',
+ ],
+ },
+
+ // 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'],
+
+ // 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',
+
+ // 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',
+ ],
+
+ // 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/docs/FEATURE_FLOW.md b/docs/FEATURE_FLOW.md
deleted file mode 100644
index c5e8878b..00000000
--- a/docs/FEATURE_FLOW.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# Flow
-
-This is a feature branch of `react-universally` that is currently built against `v11.0.1`.
-
-It provides you with a [Flow](https://flowtype.org/) implementation. Flow is a static type checker for javascript that uses a lot of type inference, so you don't need to declare types throughout your code.
-
-Most modern IDEs have a Flow plugin available which will report errors and use the Flow data to provide you with things like autocomplete w/ type information.
-
-## Additional `package.json` Scripts
-
-The following commands are added to the `package.json` scripts.
-
-## `npm run flow`
-
-Executes `flow-bin`, performing flow based type checking on the source. If you really like flow I would recommend getting a plugin for your IDE. For Atom I recommend `flow-ide`.
-
-## `npm run flow:defs`
-
-Installs the flow type definitions for the projects dependencies from the official "flow-typed" repository.
-
-## `npm run flow:coverage`
-
-Executes `flow-coverage-report`, generating a report on your type check coverage. It returns with an error if your coverage is below 80%. After you have run it I recommend clicking into the generated flow-coverage directory and opening the HTML report. You can click through into files to see where your coverage is lacking.
diff --git a/docs/FEATURE_REDUX_OPINIONATED.md b/docs/FEATURE_REDUX_OPINIONATED.md
deleted file mode 100644
index d24ca457..00000000
--- a/docs/FEATURE_REDUX_OPINIONATED.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Redux - Opinionated
-
-This is a feature branch of `react-universally` that is up to with version `v12.0.0`.
-
-It provides you with an opinionated implementation of Redux, making use of Redux Thunk as a middleware, as well as the React Jobs library to provide a data prefetching technique that works across the server and client. Additionally it merges in the `feature/flow` branch so that we can use Flow types to declare the Redux actions.
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 87%
rename from tools/development/createVendorDLL.js
rename to internal/development/createVendorDLL.js
index b705501a..10e39bec 100644
--- a/tools/development/createVendorDLL.js
+++ b/internal/development/createVendorDLL.js
@@ -1,5 +1,3 @@
-/* @flow */
-
import webpack from 'webpack';
import { resolve as pathResolve } from 'path';
import appRootDir from 'app-root-dir';
@@ -8,10 +6,9 @@ import fs from 'fs';
import config from '../../config';
import { log } from '../utils';
-function createVendorDLL(bundleName : string, bundleConfig : Object) {
- const dllConfig = config.bundles.client.devVendorDLL;
+function createVendorDLL(bundleName, bundleConfig) {
+ const dllConfig = config('bundles.client.devVendorDLL');
- // $FlowFixMe
const pkg = require(pathResolve(appRootDir.get(), './package.json'));
const devDLLDependencies = dllConfig.include.sort();
@@ -19,14 +16,14 @@ function createVendorDLL(bundleName : string, bundleConfig : Object) {
// 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(),
@@ -48,11 +45,7 @@ function createVendorDLL(bundleName : string, bundleConfig : Object) {
},
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,
}),
],
@@ -74,7 +67,7 @@ function createVendorDLL(bundleName : string, bundleConfig : Object) {
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 87%
rename from tools/development/hotClientServer.js
rename to internal/development/hotClientServer.js
index 7e8ab012..e7d7eff3 100644
--- a/tools/development/hotClientServer.js
+++ b/internal/development/hotClientServer.js
@@ -1,16 +1,12 @@
-/* @flow */
-
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 {
- webpackDevMiddleware: any;
- listenerManager: ListenerManager;
-
- constructor(compiler : Object) {
+ constructor(compiler) {
const app = express();
const httpPathRegex = /^https?:\/\/(.*):([\d]{1,5})/i;
@@ -28,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
@@ -39,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');
@@ -74,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 57%
rename from tools/development/hotDevelopment.js
rename to internal/development/hotDevelopment.js
index 54951a1b..6527a0e9 100644
--- a/tools/development/hotDevelopment.js
+++ b/internal/development/hotDevelopment.js
@@ -1,5 +1,3 @@
-/* @flow */
-
import { resolve as pathResolve } from 'path';
import webpack from 'webpack';
import appRootDir from 'app-root-dir';
@@ -37,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`,
+ )),
}),
);
}
@@ -60,63 +55,61 @@ const initializeBundle = (name, bundleConfig) => {
throw err;
}
};
+
return { name, bundleConfig, createCompiler };
};
class HotDevelopment {
- hotNodeServers: Array;
- hotClientServer: ?HotClientServer;
-
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 90%
rename from tools/development/hotNodeServer.js
rename to internal/development/hotNodeServer.js
index 8ba0ffd8..7129d2e2 100644
--- a/tools/development/hotNodeServer.js
+++ b/internal/development/hotNodeServer.js
@@ -1,18 +1,10 @@
-/* @flow */
-
import path from 'path';
import appRootDir from 'app-root-dir';
import { spawn } from 'child_process';
import { log } from '../utils';
class HotNodeServer {
- watcher: any;
- disposing: bool;
- server: ?Object;
- serverCompiling: bool;
- clientCompiling: bool;
-
- constructor(name: string, compiler : Object, clientCompiler : Object) {
+ constructor(name, compiler, clientCompiler) {
const compiledEntryFile = path.resolve(
appRootDir.get(),
compiler.options.output.path,
@@ -30,7 +22,7 @@ class HotNodeServer {
});
}
- const newServer = spawn('node', [compiledEntryFile]);
+ const newServer = spawn('node', [compiledEntryFile, '--color']);
log({
title: name,
@@ -127,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 4d7a8f20..44896298 100644
--- a/tools/development/index.js
+++ b/internal/development/index.js
@@ -1,5 +1,3 @@
-/* @flow */
-
import chokidar from 'chokidar';
import { resolve as pathResolve } from 'path';
import appRootDir from 'app-root-dir';
@@ -10,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({
@@ -25,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 89%
rename from tools/development/listenerManager.js
rename to internal/development/listenerManager.js
index 4fdf8e34..696e6673 100644
--- a/tools/development/listenerManager.js
+++ b/internal/development/listenerManager.js
@@ -1,14 +1,7 @@
-/* @flow */
-
const { log } = require('../utils');
class ListenerManager {
- name: string;
- lastConnectionKey: number;
- connectionMap: { [key: string|number]: Object };
- listener: Object;
-
- constructor(listener : Object, name : string) {
+ constructor(listener, name) {
this.name = name || 'listener';
this.lastConnectionKey = 0;
this.connectionMap = {};
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 51%
rename from tools/scripts/deploy.js
rename to internal/scripts/deploy.js
index 1e28928a..4e915e95 100644
--- a/tools/scripts/deploy.js
+++ b/internal/scripts/deploy.js
@@ -1,7 +1,7 @@
-/* @flow */
-
-// 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..4f05f96e
--- /dev/null
+++ b/internal/webpack/configFactory.js
@@ -0,0 +1,521 @@
+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,
+ }),
+ ),
+
+ // 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: {
+ rules: 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'],
+ }),
+ ),
+ ),
+
+ // 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)(() => ({
+ test: new RegExp(`\\.(${config('bundleAssetTypes').join('|')})$`, 'i'),
+ loader: 'file-loader',
+ 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,
+ },
+ })),
+
+ // 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',
+ }),
+ ]),
+ },
+ };
+
+ 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.json b/package.json
index 52bbac13..bf07a09b 100644
--- a/package.json
+++ b/package.json
@@ -1,25 +1,43 @@
{
"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",
- "flow": "babel-node tools/scripts/flow",
- "flow:defs": "flow-typed install --overwrite",
- "flow:coverage": "flow-coverage-report -i 'src/**/*.js' -t html -t json -t text"
+ "test:coverage": "jest --coverage"
+ },
+ "lint-staged": {
+ "*.js": [
+ "prettier-eslint --write",
+ "git add"
+ ]
+ },
+ "jest": {
+ "collectCoverageFrom": [
+ "shared/**/*.{js,jsx}"
+ ],
+ "snapshotSerializers": [
+ "/node_modules/enzyme-to-json/serializer"
+ ],
+ "testPathIgnorePatterns": [
+ "/(build|internal|node_modules|flow-typed|public)/"
+ ]
},
"repository": {
"type": "git",
@@ -34,20 +52,6 @@
"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"
@@ -61,81 +65,87 @@
"/node_modules/enzyme-to-json/serializer"
],
"testPathIgnorePatterns": [
- "/(build|tools|node_modules|flow-typed|public)/"
+ "/(build|internal|node_modules|public)/"
]
},
"dependencies": {
"app-root-dir": "1.0.2",
- "axios": "0.15.3",
- "code-split-component": "2.0.0-alpha.5",
+ "axios": "0.16.1",
"colors": "1.1.2",
"compression": "1.6.2",
+ "cross-env": "4.0.0",
"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-jobs": "0.6.1",
- "react-redux": "5.0.2",
- "react-router": "4.0.0-alpha.6",
+ "express": "4.15.2",
+ "helmet": "3.5.0",
+ "hpp": "0.2.2",
+ "modernizr": "3.5.0",
+ "normalize.css": "6.0.0",
+ "offline-plugin": "4.7.0",
+ "prop-types": "15.5.8",
+ "react": "15.5.4",
+ "react-async-bootstrapper": "1.1.1",
+ "react-async-component": "1.0.0-beta.3",
+ "react-dom": "15.5.4",
+ "react-helmet": "5.0.3",
+ "react-jobs": "1.0.0-beta.2",
+ "react-redux": "5.0.4",
+ "react-router-dom": "4.1.1",
"redux": "3.6.0",
- "redux-thunk": "2.1.0",
+ "redux-thunk": "2.2.0",
"serialize-javascript": "1.3.0",
- "user-home": "2.0.0",
"uuid": "3.0.1"
},
"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",
+ "assets-webpack-plugin": "3.5.1",
+ "babel-cli": "6.24.1",
+ "babel-core": "6.24.1",
+ "babel-eslint": "7.2.2",
+ "babel-jest": "19.0.0",
+ "babel-loader": "6.4.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.4.0",
+ "babel-preset-react": "6.24.1",
+ "babel-preset-stage-3": "6.24.1",
+ "babel-template": "6.24.1",
"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",
+ "css-loader": "0.28.0",
+ "enzyme": "2.8.2",
+ "enzyme-to-json": "1.5.1",
+ "eslint": "3.19.0",
+ "eslint-config-airbnb": "14.1.0",
"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",
- "flow-bin": "0.37.4",
- "flow-coverage-report": "0.2.0",
- "flow-typed": "2.0.0",
+ "eslint-plugin-jsx-a11y": "4.0.0",
+ "eslint-plugin-react": "6.10.3",
+ "extract-text-webpack-plugin": "2.1.0",
+ "file-loader": "0.11.1",
"glob": "7.1.1",
- "happypack": "3.0.2",
- "html-webpack-plugin": "2.26.0",
- "jest": "18.1.0",
+ "happypack": "3.0.3",
+ "html-webpack-plugin": "2.28.0",
+ "husky": "0.13.3",
+ "jest": "19.0.2",
+ "lint-staged": "3.4.0",
"md5": "2.2.1",
- "node-notifier": "4.6.1",
- "react-addons-test-utils": "15.4.2",
+ "modernizr-loader": "1.0.1",
+ "node-notifier": "5.1.2",
+ "prettier": "1.2.2",
+ "prettier-eslint": "5.1.0",
+ "prettier-eslint-cli": "3.4.1",
+ "react-addons-test-utils": "15.5.1",
"react-hot-loader": "3.0.0-beta.6",
- "regenerator-runtime": "0.10.1",
- "rimraf": "2.5.4",
+ "react-test-renderer": "15.5.4",
+ "regenerator-runtime": "0.10.3",
+ "rimraf": "2.6.1",
"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",
+ "source-map-support": "0.4.14",
+ "style-loader": "0.16.1",
+ "webpack": "2.4.1",
+ "webpack-bundle-analyzer": "2.4.0",
+ "webpack-dev-middleware": "1.10.1",
+ "webpack-hot-middleware": "2.18.0",
"webpack-md5-hash": "0.0.5",
"webpack-node-externals": "1.5.4"
}
diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png
deleted file mode 100644
index c380747f148939debe24924ed45dc96e2179e67b..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 1262
zcmah}X;2eZ5Pl&*LLdSO2m;C_5#=!0iV&GfFeu0&gcKD~ZjcUCh(QQN3=j@M4F-aO
z5o(~*ViiHD)z(7D0SKgs2X*iqi-n@4(g)5kInE8{G76dR!)iTV2!6KuPrg>j=0DrO)g5+k&PU7}fp
z3YZAZB_qT-gOFXG{D;^egp3F=k-ShG0J2DEjssZ)G{-W@y_17ZQZrap70mV*%
z9TQOE9M$~`h!`mHs{=ni_G$eYD%LKS46TvtVr!Z~=#mMdr{)|Q>frZ^7V)4
zNS{TG`sv#x+6k>|(+Ehnfg~6@4nW&JXyL&buAylCR%p+F)&yvYH4r!*3r(0q)cDza
zszhfi8S-czg`=xM9HopoX+{wFpkGDum@QjQ{y`pO0|kwqsFOlEvq_>sd&Js
zT#0#N6c+5KpU|#qyoFOyj~N7~S}WIaPIiU0D2ZkLNTfufeuAjq2GawWw0`4;hoD`+
zjW!I_|11-Gk|~kA%)^ReV&!PHauuCsY*G6~PVa=aIgmg;y14kau5{;mBBfG4Gtc)R
zSkx_?DDBKIWGs`ut=>4Pk8_M=JNuGprx(Pg?6(hA*zO;!?tF8EY=3Q0kwr3Xye;ID
z7|N$By{6wCCT4xm=XAGQdn#s^UO%p|A*~DNI2(ypTLxo}gw%-boC|%XyZW#}5`;&E
zlAciG7G3tKDjHGUq|0|wf2>rKh!M=eJMTSnFK3*U=F%B+3(7LH
zeb<7#gWRM^!S!scaOmCN7pSpsY8w#@j$(Vsujzc)%brBXbnRo%Iwr%a4o
zcyWK@cLfVnFNYN^EqgT@kA#O!>ks}p`)Y1JW%dtQdUCyV{$ba#40gAI}H!V!I}
uPA9rw<++SLxHqgB(>Q5JqoS}ccle??J8OYHWTOn1K;fa$oUV}M@_ztTycksg
diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png
deleted file mode 100644
index bbe193ee0230e340d79163d5145ba81bbbcdbd0d..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 2236
zcmV;t2t)UYP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006
zVoOIv0RI600RN!9r;`8x00(qQO+^Rb3K|j!6Z}D5s6|96AX03r
z@{na^2`Vos5F%ng2!%ih(!>CPJjvre&*Pr6`vYSULJicl`p56*%-;L^X78CjGr%|a
zKZD(>1HCmH0A$pZj)LMkjr*otLdA|$cvI_8Tjl`c9A-4kroG>(c>LBSbAU*|GO`iV
z{RlO1knM+1H~PgY0u~`p!ddlbP$yN>8vRFTrRYq1&uiE2$m^!~|#cvc!(L9nwG2
z6P?E!gNFXeF9ySZO@=xzU{yZAWf|f6mFV-m$DsvlTdPkl8i@H$L)V{(Sp06&hF}0f
zOvP0W1_nR~7I4U7yWb-(J3sZ_(uq6IVSRq@&hG*G(}RFB!GVj1=OWTP0Zy+#&-=TT
zch7$*Qc23J$Tlei6)$@qfnkFjL?8M+%09tC5#0(f##}a$#=yGFDz26UXR-F!UnziI
z7!JrzC#nzr7JA?}AWNn5C1BpJEYx+ZOXDiu6@wsT2WIA`qpCR)-d)|g!=;KE&RF`m
zL3gaFZPWBONB2exfECxp|F3<3mJS8L-MCWu46;T(i7vfVn;5pc^00fr(|Gn$8UmV$
z643`G$i(Z%Yf)6)8d3yTY=Vao-d(=vfbJ4Erx7ojOw&fML@YK8nPflj0nkRf)^%k6e1H
zrcEy#w*6$P;*eSX)-yL1aKkB}#o2(66JY7jVd%s{44)$K&n*@{T>BcLjq@@y-bcgb
z#tjiYmVd3EDvO$aY2IlUu0%#hO{<^a%qI(8$r#&_6sK%$2pUaIvxnVW01*Mt_64|<
z2qBhX*v}{8zR4U%H^p%1)jvb8pP!nz%@b$~u8bP?7$W*o`BX(5yfei!J<%n9))3S^
zHO<;W&Hymxig(F>IbYebFS~zoykH0l&a6)+=KMzhwY35-4grLc3|U!@;Zq*N;PD(K
ze=||?+G@niXYMlR2b+C8rZxwS)i%+WRv`)AkmPj~v_yri7SfmyO9uDejl#ghZbm2bK>8WM?WAbp^gNZ7gzr#Bgw}fg>AM
zfUpo;3V*64!S5w6fo@2n#94K>1oc#RzZ1)brMC|obM{x^t%@cqPLY(bVfAqfWn)3*<3mA%4>NxzyQV4YcBuDt@qyvCc0t0L5
zsCjaZm@M(DWxNK8m4riF)}Kw#lGF{W7#B2obP9}xc^jS4qE^&IdQJ&ckp&wiI{G(xy3i6
z`UmfnW7h{&NX+t2Yz-M}Et@*LV-<;4r6oG^h)_+O!6wRRXwiyH)9zt?UM5-M)hY4n
z7>*%@ui#(BGjMkAY$W$x37|uA0N7Q~7rk>*&@Enl+vk>NC=x&0ksSdl5fsLN>e4ze
zo4z%EK-c*0@hU(B#sH@xHe8r?ZyBR~s{zKZ?kw9PmNy+=g5H&5RiYV&lcaGOgtM-;(#Q>18<_Hp;
za@N_)!MCG^HJ~FKr$g-WIHYZXs5wP9?diP5Cf8%=
z#uG?%E28{zFe9wTvMr15BEZi80}K~bNtAVoQ%;{;P!w45-eY(wJN191{03hy{tZ^!
zFoblj=cWJv03~!qSaf7zbY(hYa%Ew3WdJfTF*YqRFfB4RR53U@FgQ9fIV&(SIxsNJ
zqBsKp001R)MObuXVRU6WZEs|0W_bWIFfleQF)%GMHdHYaB^>EX>4U6ba`-PAZc)PV*mhnoa6Eg2ys>@D9TUE%t_@^00Scn
zE@KN5BNI!L6ay0=M1VBIWCJ6!R3OXP)X2ol#2my2%YaCrN-hBE7ZG&wLN%2D0000<
KMNUMnLSTZC#sGr=
diff --git a/public/favicon.ico b/public/favicon.ico
index 9bfbca7b34c398b6a36c87989210610a1f62b4d3..a2f9dad1889acc33bd6b7cab9fa082d27502c81a 100644
GIT binary patch
literal 34494
zcmdsg3tUv!wfC6^!!QiPFbo3>12c?~Fi4Rnw28ltUNC+YzG1hpE
zHOA&zV~wFEHG~jijWNcihWdGJKCd;_di{B=HPrg4KWhy&UV?;hzW+LB_HbsH;U(VK
zY<`Ee&p!LH_TFo+z4qE`@8LKZ=fmms9H5QMQ*vAu$8lj{;`;_ajysGnn@xPLM!bnS
zj*E|Xyg$HkH={T%DM@@Ekj!!0#&g^NUxUy2>MYur@=ImbZth2D~&o
zth%E68}C10cA9%$ekz&vXNx21CFxszl`>U;pIrCOb5W-$d_tTy
z(>U=L>jDNXoTavm9m?{M$vAoLgy6L-ZC$bL`nKGNdVH7DvuT0(0DW6GC#=pld~|%k
z;Kg%-#=Kad&|1)IXmgWKf0lONs_4ex0EG}v-{9o=lZ_Sh{f#A-!xZ5E;H*?lzqDjS
z;wz=VP(3J0J%XkG@CDm-N<;C;MF#s=`h}_NuRk4eXsF$9gc!*0Id31c>u)>^_
zhXdv?99~E_ZFzZacvZ<0rcL2NO0#Ra_BY_JF;3V)?U}J?VQzix^R_FL$FEDGZ$ucB
z7L=)OkL}rqHZ(`(Hk=(;^bJ2LEqn7P6$AL4`TdTsofWntYfRuA^trPwJm
zBF8mb@$QRwFXQ3cAL27a++arn|B{sN?qzi#=CoWGL@thV1iL}SG5*$&sMgPHa`%M=9@#^SX;E!XGw=_R0ioHwy6ehf59Lo!)7&j5llt07$ZSXYu
z_LDhb)t^?_n+~n9^B=8>zAhcFih#&Y>6VqT=y5{{%a_5Yk`15=jxz-+O=X#;O;!1k
zC+S<|lcBpQTow9AY*gNr)9T1^WAMuA&;ernm9@7^d;5SVZe-~eBn6d$?_Hy|0{?1_
zHu=~h7X0y`!av*)HsjN3A7dP5Ji$rx=gbBl8}S-}@42i~=Gp&IBUc+-#*0y>F#c+J)H&831gWgYF-L(WGj*6(SpLML
ziqk{3vbe9zH}AtYxgl{=nRZ}yI_8W)rnK6fG`=aE9?(LPd%L8MaPlcuX)
z^Db(iJu9qdw>=$k5aAera(QU_zMbmG#CZRh%#`49uT@ZddI(Q(K0uv;JK;8YXh1e4
zb2y48xJmH=6ERO}w=6UtEJ_R6Ix9h+O=?TxFv4fovymqb
z2{b`;(U1S6%--~%**BKbCO+h!f^qlyi%*8VL-RawJZYX(fWry(M4qEUpV+rc=Hssp
z&G@)Vsk54c{1tlSd7HHlweJ%n181Qf>bB)t4sDztzGuV(no(|Zkh9%b0SxMf7Gz^i
zFGE`y!e{<=ccj_WU+80JJK6d5bHl3QZR)tWWA*dRL7mD(&h|n6PQP)|u64TjXR~w<
z=VvD_J#c1sf%PoL{Y`2g!vB59N}#O-X&NtnhBm8@SO(G56Cb@}E4X#tUc~bwXZ)n*
zxztbx|LMm=mO4Q+xNNeqaO%(iHpW;Ul&93s%5Z_`XI<>S8v1QWOHx`Cv9o`WTmpjE8
zZiwBqAiQ>8`QVm)6+=W`jXFUdybfMkOf*Uf;TVqMTPOdOfp(K`_^23#CQJu@8_y|x
zm1dLP0@|N4^^t&C!}|G+!QALg#$x4ogi3usvrM5@LWZQN9HZ1nZk)KY5V8Zcx5qRd
z!U=m8m-z*#jv5~?_tk4CUy7sfHyf!u`si>J&YUMd%ePbLV|dV_GRE0uXdCb<4axu~
z2w*Z)XK7j4okGZ#@tXeW3E&$lpP&aVG9Rsm=(p2E_{g^-79$H73k#jF`r`$e->v|6CL-dGaHm<%t2ptDAUQ$+vmSe%k9l*k6a+oCv5>a&h*|&>p5>_
zL$+*!+$YFB&Nyfb@S6O-Xav1Ru6u1!#AnXD-Rit-ig7LV`}(=z)h=-$XN?==pWGKX
z%R1_=0|y
zvwn2ug+9gFtqE(w{r~=S#8J>ip?l(JlPZenK$<3RQg8*$Ip*yDtkpeIm_s;(q*PS`
zpH1X<&jbB9N}8VeNp=SBo8ul|`i}A~(3fm$Ph)R_-iX!#kfHTjOU|FZ(AcMq1wQhy
z8J|=`F3FLmCqI?J(n}%9DEQm~UrBmegKl^$Kk^7mSDIm}g#O|*i{oP$5Cs}MtqL37
zpL7hCoHIxKq9>D_=ybCa^!Y5k6z*Iq^tUsfy9fF&TVkw#62_tldOF}>=Sk*={JcvV
zSJQv`80dOV$TcQC@G(a{bM*x9v9UNPzd%|p^MCNXtpRbo)<1J}V1|!ErhGVB9ltUy
zWF6Xl5Ai0fA)Gvbm7yO~nG^nG{eH+LO7zDt%fkOY3Vek5JoS+PteLPzMSn{lj4K{%
zz7&=RL5;6W4cQb58%~b;3+q#?V;9>(d~9y(1!*$)Wpd-BH#de(-@BRo+J|y7flnjmJJ?p8?WuX3dlW$x3Dc`=V7
z1}ASV4Nm^qdU)kD27j7k-pk5m5c-Q7Si_~DfA>Oet_81afdA0UF@f_T8>l-ku;W*v
zfyk%7IsuwUCK(a!Bmwfc2}X=cRc}AqP+161|e=seF3EXCK|4
zFr#!MmHU4AXuh_5RO`&qffJuk57|iL3~St>uProhm(If;bM9s!=}a*<9i+0OSAO3b
zG4BtxqsOJ^Q2n!B*vBs{+08?zd;xN3V{y7^(_pK*|3i_!{h=GyKu4y>eB9$P;Eo#R
zW`lL<-_JpBRH`4jDm!LYT}^fILziDKo6Wyjmg?k5G(TcKyh-{#$bikzF|?qanx30#
ztVEu@9uI_%e)O89w5E+*^0&k4sN{I?920aFyODqE2NlDGzF;)LAo%meDWWdmuF9tJ
z1BT?KYad#Wf_;fWe$i9_%y3G)pbE`-thJ#)~(K|V}Z_=gGfCiIC=m`sj-k?K&oM1kl33DrS;BGd;~
zzKsjy+ho2xuXu@XQ~d3%R>kk0YgYVjWV7#IKWa>D_;X`o{XZHLZ#6Y0e)HY(ME=fR
z%bM@c>G`&E;XI!g$n$&g9FZYA6VLxu!ShYX?@lH1ZbTVw?tAf_8Y>=84xVo4DLnBH
z7Dn)H{75c1ArGvXcyf!lhd}!(?GMeFkdaR1PYy1E9@WqVUJPf#?_N4N_LwIE
zFJVp1_DZ}#np@}_=G*I7OXm?s?j~~5`bgWAC1+vu-Yq}mJr%95(8jIeoQHil%)uS~
z;y#6YIQ7SlJj-6_EZBaWr*bKbg)_etGJK_RJcUWW%SnIs>Qm-DB7M*vK|7ta`=#OD
z{m_}8ggpZ3U8Hz1KjG4+j@DO@@pe&LOX-BshBQp{+o5l_okn}#==e>{1ABTWV4L7g
zf6LlVS`QW`t!KCVSf{CB(=r8nvjW`^PP9kE@OSr<&W857vBprM&uYBIe{-p&7Pb`(
zFT$1K(;1Y;V}1r%UM#&cocaKA|K)aC@kBpj*fW!Y3y@IAgYz@v?3FJ1L
zpLOC;M@6LV%Ie0#=)F@vNkAw&P1GxT<2hnD@W1Amvz&QSdg$v5UPuoqU6^dh6Zuj*
zkHVhy1_=&w+JChy`tvbs{>_V!b*XNY7kl9C;P(_Jl#h8z13keu^m}W2-%ELml#ayk
z2f|f0Nf|=9(21z9#<>Z(e|LL6Vp^<48=dnTWTvng2X{s-xqLoq>Gk8#Zylq(UP7=p
z4*P4F0-b<AA?Q_a$?HFD7-7Qn>j9^?sf1KCMUhEv#$mG
zrL^MN4_T*7l0Pd)fMXuii~_CPa^~qe2ex27Fk#I#AbLg1HPf{B)-rsdGtPicPG9O=QH9&gA6EjZ)^vHd;K5BPiC3s0D~;HiS%cHgKN4dWRei~plYK=bYV
z2X>bGp(mt%X3ixa76>SW`}@k|*pE?@EGN*OgN~RVHg=U_T)`$WkLbqFmXbhWGNnKC
z_kXCeKKqXg%D{f4Z(?Ohq2LFPTQ1@*p%EAQ>BnPF?QDon7^jq`TMzPGx-=!Y6ngMO
zQW~mRX+1%@k5^zPg1j6~CO1vpvkg4-GVFeA6o@gQj+p=Dk%$GS4@0lul_$Z@YO~NL
zgtO4*#H(HVoOB+Jg1QB0Ov9dK*OQM1W?_!F$>@jjUzK6nv>`ie+s|_?M;)07?I`HN
z=6`t*ZGIAKUO`rZZpvhR_QnO%v>KG(LG}=mV=r)*h&Q#r$R`DTI{6*-fL&)TA`9h6
z%k<`lEtm!LQ9|FbCl?1B(*}Vqfj@Y3M%r*)8v67UKC`|QvI4$F;Cn7&{;4|fPzO&M
z9|zjH0UMg-jykY30tEgw0^S0?gmcFSGH|`5ES8?iWPTS2eQn<0C|_ZJfZ||(z>Ge<
z4m;^9q&K86$W4y%O4+oxjoLaP7Ch-j$kcZ?vNqQZD@wx{zZE#VI7Nsp`3L-~ob8YH
z5&K%CAL<8s$NKARPxk9yniX2gia99A?1dfg`A
zyHJcrJn2`3(S;h?EziXt2uqF8
zF`6+H3F{yY^&M>Kl3@3xr7(wc&&TbZ;rm^a|G>GV2ZB`PBR8eKyAAYp7UP4~bV9kq
zK%XR&c%mC=807r(&=(U93||2bi13Yq4z~dMOl87A|AdTDfwNc~+Ly70_&CdA;Svb1
z7YALMDYbe#X#YCqIUB_y*$6h4pGD07<00@Q=h_-$Y6{vI_M~F_vpI*+59=53q1Ukn
zai#-qmQTu}uOMv;WMe_@ho8rJf}ZRVP&<%whaB-u;98G1KLZyq)zB|YRn27AFbMO+=c$iaUYO<@kJgY1>FbE%~c^6|Qo4ATbGwUy`#{c{rYbJY-=
zIu`p2Lf=q%4##k#S9iasca~mG_FTk6Bj$a%pX`Y!y`ODjEb%0==Vx_*%%~LCTpjt3
zNJqk&gk%zV(e#iFtWJ=Da&cZ_6KE$nLTqCOMY322!ESR0jgMuSL)JOy
zgls~v{&J@yaZHrY4BZUANMl%%cdz}9YIhHS{^hVMt^)1f#9AQ+x^7L>lD~aHHlb-R
zeBOGnB8G=Oz`|cVV>u%7A~ugPz9h;<`$}wQz&~S+JT%ql!iI|#FH!wP9L>C
z`}ecv>=V^?+P@U`Fx&XNk~jF#5BiVAS_bwk)ZP;O9ex@c6i&AM(2b6O{&_ig*F-7+
zj^WS~zk3>b_w=wlw*J{A`nlfsSqw?Kq#6Z9K;FZOc<<
z4~%^B)k+!%hbsE>+siWfH_B%4&;y=^JQuALG!#nyz@wmA&&UYr@(*o^B4Fj#YYAG
zGvxZcBoC9`@X-O9QH1{+c@dkyi`cyE%-?|*!^u-Ph26y?mt*Z*!RW`b;7`YVBOZ&z
zx~Jxuyk$p;U;eUu5})w=fAfzQpX6cVAkaRes}G88mtjL!0=xKZ=-u)Prx{DhrVq~+
zJeAl3C{7)wo8_>r@q{OKTP05yUM2W
zHRWT(wK4jH&2R1UlDph3#-Vj0?91Z2K#uhbgVN~IFjjy1?&60nODt?BYH;3ULv-Qy
zXToP4siruPDOEV1QHF8QM6^WszF$5H{g5Emh2{uHxX>pKKaCr#Z+AnEGdRkkGRb+u
zhvCv|h`s-0^W6WtAGR1ZWUCeY_`m0>ElFXj(4jF@CdO%h;Ip09vbE(S9dtzVo5)K=
zdb0CmAB(wTA8h4G{_hSxRHwUku;&^JTOL!ukfo`xZ5l-x`VY(=8lLsrI%(!O2csw5
zF7!X^U|;bL)+YO)FRj7*X3TrpQ8@n~jo$-5tE)5#g)!HupXOyml*49GjdKU$8U^be
zItL_%2SU;bRLcE~Duvc8=msd9Wqucapct$ZBfWd`3*TU)ppIO8u@37?yAXi(73@gpMRY>GI0vGZDV^s)yye;3aF(uD
z5VkHI=mn??dBFxGjrs2p6kDk;|6|-b#&cih+ErfEUgQ|;wLf^m8wIAc57r8~;UD%z
zG}v3Cd4QapbbPSK=}&$kMErL*ewGd8!Sazp=BIcpjubl66OF@$usZVTh9fjKC_c`l
znNTN;T`{f7^swE}K9%MOX&ycCJIlL^JiL|%eY;*AnQU{(TeJ)Fn#Lt=59%da3(+}q
zzR_1WZ_d}tM96V#D7=m15(&0)oK<{}$60j>FLA(fI0(!r?T$f1j+r2Q90{pI0gm>1#fC}%e
zD&Pv(rcy2-$aAW%1o$E+!lPV?@csu!{r_5zLt%(_wLu|3zRW5@G#Q1H6W~`O{G7eJ
z!S5aKcU-~;pyQJ8pz7VR(?YEK&ykK4`WOxLN`vqu;7M`7SknKJUhV$F`tC3yTXd`|
z;$RP70o#{jkk@YsIx5KTQb^%@Rzw}cncA`u5BLqDwz|8Lf7sMyBLkhuc|Vs}}M9w(=LT?6_M(tY7*KLz)*Pe;9OoJs$tr
z&?mds0e3;+?i!JoK!?5^`_*SXwa407p_T4wf2en|<%Io|M?5{@PGN|D4K`_{<8pCy
z_5o*#cT*hb8gp?kPY#U}_k74E7VS<;1CI{-*#=+pqjrxU`!EppV=CCyEk=Dj%C_!!
zvv^6kE>sVR_U(GRBd8!KZDk|*x)#Cii1`v{li8Vr4)~EB4CaK(vmeu^Lk_Iga2{tqxXaMIm
zg}olyH^lk+y3Xv{IuzW^fWcAf0qDa^p782!*zo>-f)3tOkv>Y98tqDUq0OLm!8XxT
zzMJNUZ<40R*(|cbYQf&tXO@NMchjC%$mHFy(dt_VTOi@yNKZI*Ck%ai8#c*IN8bTC
zXZ^dFL(n!?FmDKa1p1%>*jKs<`{awD`!I|H*yWu13~kUFl(0Uhuk_Nij)#5asqWO%
zTO8OXAL>k|$J$vf*lTrBf7)AOHny;@I7$5-F!*WVOdvI?u!p|%?`IL-q#v~=iNfy2
zk-Q7rjMu%@tGnT_*DLA@k0&411HrHo)#M7EC2)@5Ix#JyC4jT(h_*Wu0|9qz9vpuJ7^x^(vbUZM5e
zQ{#eiaCb@po(kCJZGj!bcFda|=RS9Q$i@xl(>&fg(O#~-73bXrJ67n0HL&+TjJmey
zM_0^o=7%#FAyfA(N87Z*?xF@~nY&d!OFsklj_u`jlt(;$`*^keI_}dWdy~6zWJfI6
z<9Oo*-~qrdofQK6bPMcDF5qd@^b_}a=^Jbap!YwumF7j$^bgndgdakOx6V@?y@g>9
zzd3zmU@F6@HxPYlnS=SCY^cBsJLncL4qMPJ!W@h{l5v)!-Qn)f_~_-$H(~33iSCzS
zUmzE%=`0B7qg6kud=?Au3c_}=M{B&bv%@x%UZkshJNpishW|59S3jkuorTX%e>|5Ej-4o
z3d0>Xb?iIzUm36)aam(P7gCJ7U^O*Gwo~A#M_61bq`L>hXa9#F2Zqi3Y(3puz`o-?
z3)sef-)zbGucJPOflm5h-=z@szu>K|p2D|0W!@!C-&6lw+*h~g@t{m;I@k`6g1)Fh
zEYIG8vs1(9JJunwAC=k;N#jcWxaUk4x#V&^@qR>upJ>HLdI}f<$(cO3K1IbaV6QZ!v3KSHb2fW
zk2Z+072Ljt=OX4yi?jrszgA<9^0UtB+u|Xauh0FEg>i>WHO-6RvyWj;Xdme8^dNET2J(W`I6*nmo~tdwRLyT
z2YmYoyxW)?ufZlfh80UT
zq!!o=q77OChd&Rz+UMHTM|5fK{p{_X)t~aky=*b)b3uj!%{NaUp&Ns9wUuLI{fEJZ
z+4w{4_v22tY^o>jCpby=!K@8v%>eo!ol;{!TwXj2$K4E--s?!3gh_%Fho
zfHQzwC3ib;_&uPpG)6!lhc!WwTGZvddp~#ej(f7fWAAT=`Csp#_tuc9)p=5Ev5&BW
z=892E4%XnTjxdJl4s6ix(XQ%Fd>(Va9{ko%666~fUMel)e(-A^$c(WqYrH~x2;h6d
zysaCyG8ym9RDXNX_k8iHp9pg`bhyPh@7!%(h&iqmH1E=`_d}iCaez*Q=7c8H`MUnm
zmr}4s7JhF+tUu2`P;`givU&mCy)z4EM8lw)y4^Jmh%yr9`lgDk&{EtTU?e#JPm1LC
z7u-?#!QSI_2;mtkNz8K2N_lLcGr&>Z8FLE{XKG*wa=8YsGmF
zdaZPvcRYgiehK!yjeUrIpJWBypaFX(_&u18ZGio@^Ux*N;eP78(GO|{vHssvp2$B~
z)Wkph{5AgLiie2)Td~F~#=VGNz|a3=ZW#S`ot6-+$TF4Sjtv(*SDzr>hne{GOT)fu
zs|yc=?z9nizoyv=zBwO~v~m8rIpLdF{g;+(6VAH!FKXp0N=u#VI`*AlC4Qp=XYuv4
z_QxIJf<6j&9ntR%n(^4g$5#)!5!$H_tJABjaQ86oFXEd7I>6cx_flu$clETmLnO%i}Pc#
zFD$O(tIAUN4=P4@th1o!AdPKj61})o!0Pu=MI8U@iufM22ljO@K}VE|?)w4zy`6XzIt;N)g1Au$d!vz}vx>E`7r4&y0)ziKQ%kO@KIA=_ETnjkj+?*D_nd6jK
zU}J_ee^%$a6o09XSsMOh_2Ue-+%RrymT}^n!ukMoG7P*^`D3cDHg?_^+=)NHQ=Jq#
ztJb>gpC@pKRgK&4%6W=@--oF}hndZD|Fa6ed*IX!hED%*6YOgidO{w0R7*(8TkCK?
z!eWex`-+Eq!ndoipvN|lKHwVeK^ep1;pAx$
zz10DF!Vdd}-s1ik!|6A~evI|dNBO`#-+zgB+}{vB>;G)VI)D#J*_|uJ*)*-T051!5>nei$=C(3|U%$#H5WH#ScU1tk@%)!#
zkq_MO-{KVCCGz~=zN}Ooo0Z}>HWkG%^o{~wCnhBg2I
literal 15086
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
Xwh)>0#HepK00000NkvXXu0mjfV=#h%
literal 0
HcmV?d00001
diff --git a/public/favicons/apple-touch-icon-120x120.png b/public/favicons/apple-touch-icon-120x120.png
new file mode 100644
index 0000000000000000000000000000000000000000..160dad8b12b311b92c36458798004a89d6f67afe
GIT binary patch
literal 12787
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 | |