diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..a882442
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,7 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1cee096
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,152 @@
+
+# Created by https://www.gitignore.io/api/node,macos,webstorm
+
+### macOS ###
+*.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Node ###
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Typescript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
+
+### WebStorm ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff:
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/dictionaries
+
+# Sensitive or high-churn files:
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.xml
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+
+# Gradle:
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Mongo Explorer plugin:
+.idea/**/mongoSettings.xml
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+### WebStorm Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+# Sonarlint plugin
+.idea/sonarlint
+
+# End of https://www.gitignore.io/api/node,macos,webstorm
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..17193b2
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,11 @@
+FROM node:6.9.5-alpine
+
+# Copy application files
+COPY ./build /usr/src/app
+WORKDIR /usr/src/app
+
+# Install Yarn and Node.js dependencies
+RUN npm install yarn --global --no-progress --silent --depth 0 && \
+ yarn install --production --no-progress
+
+CMD [ "node", "server.js" ]
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..3e95f03
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2014-present Konstantin Tarkus, KriaSoft LLC.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
index d07a1eb..757ae27 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,9 @@
+## RUN
+`npm start` for dev or `docker build -t testapp .` and `docker run testapp`
+
+## Notes:
+https://docs.google.com/document/d/1D3lCwjdCNngTRg0Q3C0paEt7lW2jILE_TwqHeOA4m0A
+
## Coding Challenge
In order to be considered for this position, you must complete the following steps.
@@ -8,28 +14,28 @@ In order to be considered for this position, you must complete the following ste
### Prerequisites
- Please note that this will require some basic knowledge and/or the ability to learn of the following:
- - [JavaScript](http://www.codecademy.com/tracks/javascript)
- - [ExpressJS](http://expressjs.com/)
- - [ReactJS](https://facebook.github.io/react/)
- - [WebpackJS](https://webpack.js.org/)
- - [ES6](http://es6-features.org/)
- - Git
- - [Docker](http://www.docker.com/)
+ - [JavaScript](http://www.codecademy.com/tracks/javascript)
+ - [ExpressJS](http://expressjs.com/)
+ - [ReactJS](https://facebook.github.io/react/)
+ - [WebpackJS](https://webpack.js.org/)
+ - [ES6](http://es6-features.org/)
+ - Git
+ - [Docker](http://www.docker.com/)
- You will need to have the following installed to complete this task
- - [NodeJS](http://www.nodejs.org/)
- - [Docker](http://www.docker.com/)
+ - [NodeJS](http://www.nodejs.org/)
+ - [Docker](http://www.docker.com/)
## Task
1. Fork this repository
2. Create a *source* folder to contain your code.
3. In the *source* directory, please create an WebpackJS/ExpressJS/ReactJS/ES6 app that accomplishes the following:
- - Connect to the [Github API](http://developer.github.com/)
- - Find the [nodejs/node](https://github.com/nodejs/node) repository
- - Find the most recent commits (choose at least 25 or more of the commits)
- - Create a route that displays the recent commits by author.
- - If the commit hash ends in a number, color that row to light blue (#E6F1F6).
+ - Connect to the [Github API](http://developer.github.com/)
+ - Find the [nodejs/node](https://github.com/nodejs/node) repository
+ - Find the most recent commits (choose at least 25 or more of the commits)
+ - Create a route that displays the recent commits by author.
+ - If the commit hash ends in a number, color that row to light blue (#E6F1F6).
4. Dockerize your application by writing a docker.yml file and test it by running the container locally.
5. Commit and Push your code to your new repository
6. Send us a pull request, we will review your code and get back to you
@@ -38,7 +44,7 @@ In order to be considered for this position, you must complete the following ste
Create the following unit tests with the testing framework of your choice:
- 1. Verify that rows ending in a number are colored light blue.
+ 1. Verify that rows ending in a number are colored light blue.
## Once Complete
1. Commit and Push your code to your new repository
@@ -48,4 +54,4 @@ Create the following unit tests with the testing framework of your choice:
- When building the react application, you are free to use any type of css/html library if you choose to
- You are free to write and modularize code any way you like just as long as you follow the requirements
- 4 spaces for indentation! No tabs!
-- If you don't know how to do something, Google is your friend!
\ No newline at end of file
+- If you don't know how to do something, Google is your friend!
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..db773e6
--- /dev/null
+++ b/package.json
@@ -0,0 +1,214 @@
+{
+ "name": "commits",
+ "version": "0.0.0",
+ "private": true,
+ "engines": {
+ "node": ">=6.5",
+ "npm": ">=3.10"
+ },
+ "browserslist": [
+ ">1%",
+ "last 4 versions",
+ "Firefox ESR",
+ "not ie < 9"
+ ],
+ "dependencies": {
+ "babel-polyfill": "^6.22.0",
+ "bluebird": "^3.4.7",
+ "body-parser": "^1.16.0",
+ "classnames": "^2.2.5",
+ "cookie-parser": "^1.4.3",
+ "core-js": "^2.4.1",
+ "express": "^4.14.1",
+ "fastclick": "^1.0.6",
+ "history": "^4.5.1",
+ "isomorphic-style-loader": "^1.1.0",
+ "node-fetch": "^1.6.3",
+ "normalize.css": "^5.0.0",
+ "nsp": "^2.6.3",
+ "pretty-error": "^2.0.2",
+ "query-string": "^4.3.1",
+ "react": "^15.4.2",
+ "react-dom": "^15.4.2",
+ "react-redux": "^5.0.2",
+ "redux": "^3.6.0",
+ "redux-logger": "^2.7.4",
+ "redux-thunk": "^2.1.0",
+ "sequelize": "^3.30.1",
+ "serialize-javascript": "^1.3.0",
+ "source-map-support": "^0.4.11",
+ "universal-router": "^2.0.0",
+ "whatwg-fetch": "^2.0.2"
+ },
+ "devDependencies": {
+ "assets-webpack-plugin": "^3.5.1",
+ "autoprefixer": "^6.7.2",
+ "babel-cli": "^6.22.2",
+ "babel-core": "^6.22.1",
+ "babel-eslint": "^7.1.1",
+ "babel-loader": "^6.2.10",
+ "babel-plugin-rewire": "^1.0.0",
+ "babel-preset-env": "^1.1.8",
+ "babel-preset-react": "^6.22.0",
+ "babel-preset-react-optimize": "^1.0.1",
+ "babel-preset-stage-2": "^6.22.0",
+ "babel-register": "^6.22.0",
+ "babel-template": "^6.22.0",
+ "babel-types": "^6.22.0",
+ "browser-sync": "^2.18.7",
+ "chai": "^3.5.0",
+ "chokidar": "^1.6.1",
+ "css-loader": "^0.26.1",
+ "editorconfig-tools": "^0.1.1",
+ "enzyme": "^2.7.1",
+ "eslint": "^3.15.0",
+ "eslint-config-airbnb": "^14.1.0",
+ "eslint-loader": "^1.6.1",
+ "eslint-plugin-css-modules": "^2.2.0",
+ "eslint-plugin-import": "^2.2.0",
+ "eslint-plugin-jsx-a11y": "^4.0.0",
+ "eslint-plugin-react": "^6.9.0",
+ "file-loader": "^0.10.0",
+ "front-matter": "^2.1.2",
+ "glob": "^7.1.1",
+ "json-loader": "^0.5.4",
+ "lint-staged": "^3.3.0",
+ "markdown-it": "^8.2.2",
+ "mkdirp": "^0.5.1",
+ "mocha": "^3.2.0",
+ "pixrem": "^3.0.2",
+ "pleeease-filters": "^3.0.0",
+ "postcss": "^5.2.12",
+ "postcss-calc": "^5.3.1",
+ "postcss-color-function": "^3.0.0",
+ "postcss-custom-media": "^5.0.1",
+ "postcss-custom-properties": "^5.0.2",
+ "postcss-custom-selectors": "^3.0.0",
+ "postcss-flexbugs-fixes": "^2.1.0",
+ "postcss-loader": "^1.2.2",
+ "postcss-media-minmax": "^2.1.2",
+ "postcss-nested": "^1.0.0",
+ "postcss-nesting": "^2.3.1",
+ "postcss-partial-import": "^3.1.0",
+ "postcss-pseudoelements": "^3.0.0",
+ "postcss-selector-matches": "^2.0.5",
+ "postcss-selector-not": "^2.0.0",
+ "postcss-url": "^5.1.2",
+ "pre-commit": "^1.2.2",
+ "raw-loader": "^0.5.1",
+ "react-addons-test-utils": "^15.4.2",
+ "react-deep-force-update": "^2.0.1",
+ "react-hot-loader": "^3.0.0-beta.6",
+ "redbox-react": "^1.3.3",
+ "redux-mock-store": "^1.2.1",
+ "rimraf": "^2.5.4",
+ "sinon": "^2.0.0-pre.5",
+ "stylefmt": "^5.1.1",
+ "stylelint": "^7.8.0",
+ "stylelint-config-standard": "^16.0.0",
+ "url-loader": "^0.5.7",
+ "webpack": "^2.2.1",
+ "webpack-bundle-analyzer": "^2.3.0",
+ "webpack-dev-middleware": "^1.10.0",
+ "webpack-hot-middleware": "^2.16.1",
+ "write-file-webpack-plugin": "^3.4.2"
+ },
+ "babel": {
+ "presets": [
+ [
+ "env",
+ {
+ "targets": {
+ "node": "current"
+ }
+ }
+ ],
+ "stage-2",
+ "react"
+ ],
+ "env": {
+ "test": {
+ "plugins": [
+ "rewire"
+ ]
+ }
+ }
+ },
+ "eslintConfig": {
+ "parser": "babel-eslint",
+ "extends": [
+ "airbnb",
+ "plugin:css-modules/recommended"
+ ],
+ "plugins": [
+ "css-modules"
+ ],
+ "globals": {
+ "__DEV__": true
+ },
+ "env": {
+ "browser": true
+ },
+ "rules": {
+ "import/extensions": "off",
+ "import/no-extraneous-dependencies": "off",
+ "react/jsx-filename-extension": "off",
+ "react/prefer-stateless-function": "off"
+ }
+ },
+ "stylelint": {
+ "extends": "stylelint-config-standard",
+ "rules": {
+ "string-quotes": "single",
+ "property-no-unknown": [
+ true,
+ {
+ "ignoreProperties": [
+ "composes"
+ ]
+ }
+ ],
+ "selector-pseudo-class-no-unknown": [
+ true,
+ {
+ "ignorePseudoClasses": [
+ "global"
+ ]
+ }
+ ]
+ }
+ },
+ "lint-staged": {
+ "*.{cmd,html,json,md,sh,txt,xml,yml}": [
+ "editorconfig-tools fix",
+ "git add"
+ ],
+ "*.{js,jsx}": [
+ "eslint --fix",
+ "git add"
+ ],
+ "*.{css,less,scss,sss}": [
+ "stylefmt",
+ "stylelint",
+ "git add"
+ ]
+ },
+ "scripts": {
+ "lint:js": "eslint src tools",
+ "lint:css": "stylelint \"src/**/*.{css,less,scss,sss}\"",
+ "lint:staged": "lint-staged",
+ "lint": "npm run lint:js && npm run lint:css",
+ "test": "mocha \"src/**/*.test.js\" --require babel-register --require test/setup.js",
+ "test:watch": "npm run test -- --reporter min --watch",
+ "nsp": "nsp check --output summary",
+ "clean": "babel-node tools/run clean",
+ "copy": "babel-node tools/run copy",
+ "bundle": "babel-node tools/run bundle",
+ "build": "babel-node tools/run build",
+ "build:stats": "npm run build -- --release --analyse",
+ "deploy": "babel-node tools/run deploy",
+ "render": "babel-node tools/run render",
+ "serve": "babel-node tools/run runServer",
+ "start": "babel-node tools/run start"
+ }
+}
diff --git a/src/actions/README.md b/src/actions/README.md
new file mode 100644
index 0000000..be6bb8f
--- /dev/null
+++ b/src/actions/README.md
@@ -0,0 +1,3 @@
+# Action creators
+
+Action Creators should go there
diff --git a/src/actions/runtime.js b/src/actions/runtime.js
new file mode 100644
index 0000000..1068088
--- /dev/null
+++ b/src/actions/runtime.js
@@ -0,0 +1,13 @@
+/* eslint-disable import/prefer-default-export */
+
+import { SET_RUNTIME_VARIABLE } from '../constants';
+
+export function setRuntimeVariable({ name, value }) {
+ return {
+ type: SET_RUNTIME_VARIABLE,
+ payload: {
+ name,
+ value,
+ },
+ };
+}
diff --git a/src/client.js b/src/client.js
new file mode 100644
index 0000000..55a8333
--- /dev/null
+++ b/src/client.js
@@ -0,0 +1,185 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import FastClick from 'fastclick';
+import UniversalRouter from 'universal-router';
+import queryString from 'query-string';
+import { createPath } from 'history/PathUtils';
+import history from './core/history';
+import App from './components/App';
+import configureStore from './store/configureStore';
+import { updateMeta } from './core/DOMUtils';
+import { ErrorReporter, deepForceUpdate } from './core/devUtils';
+
+// Global (context) variables that can be easily accessed from any React component
+// https://facebook.github.io/react/docs/context.html
+const context = {
+ // Enables critical path CSS rendering
+ // https://github.com/kriasoft/isomorphic-style-loader
+ insertCss: (...styles) => {
+ // eslint-disable-next-line no-underscore-dangle
+ const removeCss = styles.map(x => x._insertCss());
+ return () => { removeCss.forEach(f => f()); };
+ },
+ // Initialize a new Redux store
+ // http://redux.js.org/docs/basics/UsageWithReact.html
+ store: configureStore(window.APP_STATE, { history }),
+};
+
+// Switch off the native scroll restoration behavior and handle it manually
+// https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration
+const scrollPositionsHistory = {};
+if (window.history && 'scrollRestoration' in window.history) {
+ window.history.scrollRestoration = 'manual';
+}
+
+let onRenderComplete = function initialRenderComplete() {
+ const elem = document.getElementById('css');
+ if (elem) elem.parentNode.removeChild(elem);
+ onRenderComplete = function renderComplete(route, location) {
+ document.title = route.title;
+
+ updateMeta('description', route.description);
+ // Update necessary tags in
at runtime here, ie:
+ // updateMeta('keywords', route.keywords);
+ // updateCustomMeta('og:url', route.canonicalUrl);
+ // updateCustomMeta('og:image', route.imageUrl);
+ // updateLink('canonical', route.canonicalUrl);
+ // etc.
+
+ let scrollX = 0;
+ let scrollY = 0;
+ const pos = scrollPositionsHistory[location.key];
+ if (pos) {
+ scrollX = pos.scrollX;
+ scrollY = pos.scrollY;
+ } else {
+ const targetHash = location.hash.substr(1);
+ if (targetHash) {
+ const target = document.getElementById(targetHash);
+ if (target) {
+ scrollY = window.pageYOffset + target.getBoundingClientRect().top;
+ }
+ }
+ }
+
+ // Restore the scroll position if it was saved into the state
+ // or scroll to the given #hash anchor
+ // or scroll to top of the page
+ window.scrollTo(scrollX, scrollY);
+
+ // Google Analytics tracking. Don't send 'pageview' event after
+ // the initial rendering, as it was already sent
+ if (window.ga) {
+ window.ga('send', 'pageview', createPath(location));
+ }
+ };
+};
+
+// Make taps on links and buttons work fast on mobiles
+FastClick.attach(document.body);
+
+const container = document.getElementById('app');
+let appInstance;
+let currentLocation = history.location;
+let routes = require('./routes').default;
+
+// Re-render the app when window.location changes
+async function onLocationChange(location, action) {
+ // Remember the latest scroll position for the previous location
+ scrollPositionsHistory[currentLocation.key] = {
+ scrollX: window.pageXOffset,
+ scrollY: window.pageYOffset,
+ };
+ // Delete stored scroll position for next page if any
+ if (action === 'PUSH') {
+ delete scrollPositionsHistory[location.key];
+ }
+ currentLocation = location;
+
+ try {
+ // Traverses the list of routes in the order they are defined until
+ // it finds the first route that matches provided URL path string
+ // and whose action method returns anything other than `undefined`.
+ const route = await UniversalRouter.resolve(routes, {
+ ...context,
+ path: location.pathname,
+ query: queryString.parse(location.search),
+ });
+
+ // Prevent multiple page renders during the routing process
+ if (currentLocation.key !== location.key) {
+ return;
+ }
+
+ if (route.redirect) {
+ history.replace(route.redirect);
+ return;
+ }
+
+ appInstance = ReactDOM.render(
+ {route.component},
+ container,
+ () => onRenderComplete(route, location),
+ );
+ } catch (error) {
+ // Display the error in full-screen for development mode
+ if (__DEV__) {
+ appInstance = null;
+ document.title = `Error: ${error.message}`;
+ ReactDOM.render(, container);
+ throw error;
+ }
+
+ console.error(error); // eslint-disable-line no-console
+
+ // Do a full page reload if error occurs during client-side navigation
+ if (action && currentLocation.key === location.key) {
+ window.location.reload();
+ }
+ }
+}
+
+// Handle client-side navigation by using HTML5 History API
+// For more information visit https://github.com/mjackson/history#readme
+history.listen(onLocationChange);
+onLocationChange(currentLocation);
+
+// Handle errors that might happen after rendering
+// Display the error in full-screen for development mode
+if (__DEV__) {
+ window.addEventListener('error', (event) => {
+ appInstance = null;
+ document.title = `Runtime Error: ${event.error.message}`;
+ ReactDOM.render(, container);
+ });
+}
+
+// Enable Hot Module Replacement (HMR)
+if (module.hot) {
+ module.hot.accept('./routes', () => {
+ routes = require('./routes').default; // eslint-disable-line global-require
+
+ if (appInstance) {
+ try {
+ // Force-update the whole tree, including components that refuse to update
+ deepForceUpdate(appInstance);
+ } catch (error) {
+ appInstance = null;
+ document.title = `Hot Update Error: ${error.message}`;
+ ReactDOM.render(, container);
+ return;
+ }
+ }
+
+ onLocationChange(currentLocation);
+ });
+}
diff --git a/src/components/App.js b/src/components/App.js
new file mode 100644
index 0000000..4118ba8
--- /dev/null
+++ b/src/components/App.js
@@ -0,0 +1,68 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import React, { Children, PropTypes } from 'react';
+
+const ContextType = {
+ // Enables critical path CSS rendering
+ // https://github.com/kriasoft/isomorphic-style-loader
+ insertCss: PropTypes.func.isRequired,
+ // Integrate Redux
+ // http://redux.js.org/docs/basics/UsageWithReact.html
+ store: PropTypes.shape({
+ subscribe: PropTypes.func.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ getState: PropTypes.func.isRequired,
+ }).isRequired,
+};
+
+/**
+ * The top-level React component setting context (global) variables
+ * that can be accessed from all the child components.
+ *
+ * https://facebook.github.io/react/docs/context.html
+ *
+ * Usage example:
+ *
+ * const context = {
+ * history: createBrowserHistory(),
+ * store: createStore(),
+ * };
+ *
+ * ReactDOM.render(
+ *
+ *
+ *
+ *
+ * ,
+ * container,
+ * );
+ */
+class App extends React.PureComponent {
+
+ static propTypes = {
+ context: PropTypes.shape(ContextType).isRequired,
+ children: PropTypes.element.isRequired,
+ };
+
+ static childContextTypes = ContextType;
+
+ getChildContext() {
+ return this.props.context;
+ }
+
+ render() {
+ // NOTE: If you need to add or modify header, footer etc. of the app,
+ // please do that inside the Layout component.
+ return Children.only(this.props.children);
+ }
+
+}
+
+export default App;
diff --git a/src/components/Author/Author.css b/src/components/Author/Author.css
new file mode 100644
index 0000000..d200a8b
--- /dev/null
+++ b/src/components/Author/Author.css
@@ -0,0 +1,7 @@
+.commits-list {
+ padding: 10px 0;
+}
+
+a:hover {
+ color: red;
+}
diff --git a/src/components/Author/Author.js b/src/components/Author/Author.js
new file mode 100644
index 0000000..d2e479a
--- /dev/null
+++ b/src/components/Author/Author.js
@@ -0,0 +1,13 @@
+import React from 'react';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import s from './Author.css';
+import { SingleCommitComponent } from '../../components/SingleCommit/SingleCommit'
+
+export const AuthorComponent = ( {commits} ) =>
+
+
+ {commits.map((commit) => )}
+
+
;
+
+export default withStyles(s)(AuthorComponent);
diff --git a/src/components/Author/package.json b/src/components/Author/package.json
new file mode 100644
index 0000000..1f5a43f
--- /dev/null
+++ b/src/components/Author/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "Author",
+ "version": "0.0.0",
+ "private": true,
+ "main": "./Author.js"
+}
diff --git a/src/components/Commits/Commits.css b/src/components/Commits/Commits.css
new file mode 100644
index 0000000..79da71d
--- /dev/null
+++ b/src/components/Commits/Commits.css
@@ -0,0 +1,3 @@
+.root {
+ padding: 30px;
+}
diff --git a/src/components/Commits/Commits.js b/src/components/Commits/Commits.js
new file mode 100644
index 0000000..dfdb0d3
--- /dev/null
+++ b/src/components/Commits/Commits.js
@@ -0,0 +1,14 @@
+import React from 'react';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import s from './Commits.css';
+import { SingleCommitComponent } from '../SingleCommit/SingleCommit'
+
+export const CommitsComponent = ( {commits} ) =>
+
+
+
+ {commits.map((commit) => )}
+
+ ;
+
+export default withStyles(s)(CommitsComponent);
diff --git a/src/components/Commits/package.json b/src/components/Commits/package.json
new file mode 100644
index 0000000..017205f
--- /dev/null
+++ b/src/components/Commits/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "Commits",
+ "version": "0.0.0",
+ "private": true,
+ "main": "./Commits.js"
+}
diff --git a/src/components/Header/Header.css b/src/components/Header/Header.css
new file mode 100644
index 0000000..b642280
--- /dev/null
+++ b/src/components/Header/Header.css
@@ -0,0 +1,7 @@
+.root {
+ background-color: #0366D6;
+ width: 100%;
+ color: white;
+ padding: 10px 30px;
+}
+
diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js
new file mode 100644
index 0000000..8dac414
--- /dev/null
+++ b/src/components/Header/Header.js
@@ -0,0 +1,10 @@
+import React from 'react';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import s from './Header.css';
+
+export const HeaderComponent = () =>
+ ;
+
+export default withStyles(s)(HeaderComponent);
diff --git a/src/components/Header/package.json b/src/components/Header/package.json
new file mode 100644
index 0000000..a35793d
--- /dev/null
+++ b/src/components/Header/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "Header",
+ "version": "0.0.0",
+ "private": true,
+ "main": "./Header.js"
+}
diff --git a/src/components/Html.js b/src/components/Html.js
new file mode 100644
index 0000000..a00531a
--- /dev/null
+++ b/src/components/Html.js
@@ -0,0 +1,84 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import React, { PropTypes } from 'react';
+import serialize from 'serialize-javascript';
+import { analytics } from '../config';
+
+class Html extends React.Component {
+ static propTypes = {
+ title: PropTypes.string.isRequired,
+ description: PropTypes.string.isRequired,
+ styles: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ cssText: PropTypes.string.isRequired,
+ }).isRequired),
+ scripts: PropTypes.arrayOf(PropTypes.string.isRequired),
+ // eslint-disable-next-line react/forbid-prop-types
+ state: PropTypes.object,
+ children: PropTypes.string.isRequired,
+ };
+
+ static defaultProps = {
+ styles: [],
+ scripts: [],
+ state: null,
+ };
+
+ render() {
+ const { title, description, styles, scripts, state, children } = this.props;
+ return (
+
+
+
+
+ {title}
+
+
+ {styles.map(style =>
+ ,
+ )}
+
+
+
+ {state && (
+
+ )}
+ {scripts.map(script => )}
+ {analytics.google.trackingId &&
+
+ }
+ {analytics.google.trackingId &&
+
+ }
+
+
+ );
+ }
+}
+
+export default Html;
diff --git a/src/components/Layout/Layout.css b/src/components/Layout/Layout.css
new file mode 100644
index 0000000..0117467
--- /dev/null
+++ b/src/components/Layout/Layout.css
@@ -0,0 +1,170 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+@import '../../../node_modules/normalize.css/normalize.css';
+
+@import '../variables.css';
+
+
+/*
+ * Base styles
+ * ========================================================================== */
+
+html {
+ color: #222;
+ font-weight: 100;
+ font-size: 1em; /* ~16px; */
+ font-family: var(--font-family-base);
+ line-height: 1.375; /* ~22px */
+}
+
+a {
+ color: #0074c2;
+}
+
+/*
+ * Remove text-shadow in selection highlight:
+ * https://twitter.com/miketaylr/status/12228805301
+ *
+ * These selection rule sets have to be separate.
+ * Customize the background color to match your design.
+ */
+
+::-moz-selection {
+ background: #b3d4fc;
+ text-shadow: none;
+}
+
+::selection {
+ background: #b3d4fc;
+ text-shadow: none;
+}
+
+/*
+ * A better looking default horizontal rule
+ */
+
+hr {
+ display: block;
+ height: 1px;
+ border: 0;
+ border-top: 1px solid #ccc;
+ margin: 1em 0;
+ padding: 0;
+}
+
+/*
+ * Remove the gap between audio, canvas, iframes,
+ * images, videos and the bottom of their containers:
+ * https://github.com/h5bp/html5-boilerplate/issues/440
+ */
+
+audio,
+canvas,
+iframe,
+img,
+svg,
+video {
+ vertical-align: middle;
+}
+
+/*
+ * Remove default fieldset styles.
+ */
+
+fieldset {
+ border: 0;
+ margin: 0;
+ padding: 0;
+}
+
+/*
+ * Allow only vertical resizing of textareas.
+ */
+
+textarea {
+ resize: vertical;
+}
+
+
+/*
+ * Print styles
+ * Inlined to avoid the additional HTTP request:
+ * http://www.phpied.com/delay-loading-your-print-css/
+ * ========================================================================== */
+
+@media print {
+ *,
+ *::before,
+ *::after {
+ background: transparent !important;
+ color: #000 !important; /* Black prints faster: http://www.sanbeiji.com/archives/953 */
+ box-shadow: none !important;
+ text-shadow: none !important;
+ }
+
+ a,
+ a:visited {
+ text-decoration: underline;
+ }
+
+ a[href]::after {
+ content: ' (' attr(href) ')';
+ }
+
+ abbr[title]::after {
+ content: ' (' attr(title) ')';
+ }
+
+ /*
+ * Don't show links that are fragment identifiers,
+ * or use the `javascript:` pseudo protocol
+ */
+
+ a[href^='#']::after,
+ a[href^='javascript:']::after {
+ content: '';
+ }
+
+ pre,
+ blockquote {
+ border: 1px solid #999;
+ page-break-inside: avoid;
+ }
+
+ /*
+ * Printing Tables:
+ * http://css-discuss.incutio.com/wiki/Printing_Tables
+ */
+
+ thead {
+ display: table-header-group;
+ }
+
+ tr,
+ img {
+ page-break-inside: avoid;
+ }
+
+ img {
+ max-width: 100% !important;
+ }
+
+ p,
+ h2,
+ h3 {
+ orphans: 3;
+ widows: 3;
+ }
+
+ h2,
+ h3 {
+ page-break-after: avoid;
+ }
+}
diff --git a/src/components/Layout/Layout.js b/src/components/Layout/Layout.js
new file mode 100644
index 0000000..613fb32
--- /dev/null
+++ b/src/components/Layout/Layout.js
@@ -0,0 +1,30 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import React, { PropTypes } from 'react';
+import Header from './../Header';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import s from './Layout.css';
+
+class Layout extends React.Component {
+ static propTypes = {
+ children: PropTypes.node.isRequired,
+ };
+
+ render() {
+ return (
+
+
+ {this.props.children}
+
+ );
+ }
+}
+
+export default withStyles(s)(Layout);
diff --git a/src/components/Layout/Layout.test.js b/src/components/Layout/Layout.test.js
new file mode 100644
index 0000000..a933ea1
--- /dev/null
+++ b/src/components/Layout/Layout.test.js
@@ -0,0 +1,39 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+/* eslint-env mocha */
+/* eslint-disable padded-blocks, no-unused-expressions */
+
+import React from 'react';
+import { expect } from 'chai';
+import { render } from 'enzyme';
+import configureStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import App from '../App';
+import Layout from './Layout';
+
+const middlewares = [thunk];
+const mockStore = configureStore(middlewares);
+const initialState = {};
+
+describe('Layout', () => {
+ it('renders children correctly', () => {
+ const store = mockStore(initialState);
+
+ const wrapper = render(
+ {}, store }}>
+
+
+
+ ,
+ );
+ expect(wrapper.find('div.child').length).to.eq(1);
+ });
+
+});
diff --git a/src/components/Layout/package.json b/src/components/Layout/package.json
new file mode 100644
index 0000000..9d55136
--- /dev/null
+++ b/src/components/Layout/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "Layout",
+ "version": "0.0.0",
+ "private": true,
+ "main": "./Layout.js"
+}
diff --git a/src/components/Main/Main.css b/src/components/Main/Main.css
new file mode 100644
index 0000000..affd469
--- /dev/null
+++ b/src/components/Main/Main.css
@@ -0,0 +1,6 @@
+.root {
+ position: relative;
+ background: #fff;
+ margin: auto;
+ box-sizing: border-box;
+}
diff --git a/src/components/Main/Main.js b/src/components/Main/Main.js
new file mode 100644
index 0000000..d0e726b
--- /dev/null
+++ b/src/components/Main/Main.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import s from './Main.css';
+
+export const MainComponent = () =>
+
+ Hello!
+ Click here for the latest commits
+ ;
+
+export default withStyles(s)(MainComponent);
diff --git a/src/components/Main/package.json b/src/components/Main/package.json
new file mode 100644
index 0000000..86c1857
--- /dev/null
+++ b/src/components/Main/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "Main",
+ "version": "0.0.0",
+ "private": true,
+ "main": "./Main.js"
+}
diff --git a/src/components/SingleCommit/SingleCommit.css b/src/components/SingleCommit/SingleCommit.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/SingleCommit/SingleCommit.js b/src/components/SingleCommit/SingleCommit.js
new file mode 100644
index 0000000..2062572
--- /dev/null
+++ b/src/components/SingleCommit/SingleCommit.js
@@ -0,0 +1,16 @@
+import React from 'react';
+import s from './SingleCommit.css';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import { Link } from 'universal-router';
+
+export const SingleCommitComponent = ( {commit} ) =>
+
+ {commit.author.login}
+ {commit.sha}
+
+ {commit.commit.message}
+ ;
+
+export default withStyles(s)(SingleCommitComponent);
diff --git a/src/components/SingleCommit/SingleCommit.test.js b/src/components/SingleCommit/SingleCommit.test.js
new file mode 100644
index 0000000..2954b83
--- /dev/null
+++ b/src/components/SingleCommit/SingleCommit.test.js
@@ -0,0 +1,34 @@
+/* eslint-env mocha */
+/* eslint-disable no-unused-expressions */
+
+import React from 'react';
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import { SingleCommitComponent } from './SingleCommit';
+
+const data = {
+ commit: {
+ sha: '123',
+ author: {
+ login: 'Jane Doe'
+ },
+ commit: {
+ message: 'hello'
+ }
+ }
+};
+
+
+describe(`#File: ${__filename}`, () => {
+ it('should exist', () => {
+ expect(SingleCommitComponent).to.be.ok;
+ });
+});
+
+describe('Rendering', () => {
+ const wrapper = shallow();
+
+ it('should render list element', () => {
+ expect(wrapper.find('li')).to.have.length(1);
+ });
+});
diff --git a/src/components/SingleCommit/package.json b/src/components/SingleCommit/package.json
new file mode 100644
index 0000000..e14e6e6
--- /dev/null
+++ b/src/components/SingleCommit/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "Commit",
+ "version": "0.0.0",
+ "private": true,
+ "main": "./Commit.js"
+}
diff --git a/src/components/variables.css b/src/components/variables.css
new file mode 100644
index 0000000..7afb3f3
--- /dev/null
+++ b/src/components/variables.css
@@ -0,0 +1,31 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+:root {
+ /*
+ * Typography
+ * ======================================================================== */
+
+ --font-family-base: 'Segoe UI', 'HelveticaNeue-Light', sans-serif;
+
+ /*
+ * Layout
+ * ======================================================================== */
+
+ --max-content-width: 1000px;
+
+ /*
+ * Media queries breakpoints
+ * ======================================================================== */
+
+ --screen-xs-min: 480px; /* Extra small screen / phone */
+ --screen-sm-min: 768px; /* Small screen / tablet */
+ --screen-md-min: 992px; /* Medium screen / desktop */
+ --screen-lg-min: 1200px; /* Large screen / wide desktop */
+}
diff --git a/src/config.js b/src/config.js
new file mode 100644
index 0000000..fa203d9
--- /dev/null
+++ b/src/config.js
@@ -0,0 +1,22 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+/* eslint-disable max-len */
+
+export const port = process.env.PORT || 3000;
+export const host = process.env.WEBSITE_HOSTNAME || `localhost:${port}`;
+
+export const analytics = {
+
+ // https://analytics.google.com/
+ google: {
+ trackingId: process.env.GOOGLE_TRACKING_ID, // UA-XXXXX-X
+ },
+
+};
diff --git a/src/constants/index.js b/src/constants/index.js
new file mode 100644
index 0000000..ce20184
--- /dev/null
+++ b/src/constants/index.js
@@ -0,0 +1,3 @@
+/* eslint-disable import/prefer-default-export */
+
+export const SET_RUNTIME_VARIABLE = 'SET_RUNTIME_VARIABLE';
diff --git a/src/core/DOMUtils.js b/src/core/DOMUtils.js
new file mode 100644
index 0000000..bb7c0cb
--- /dev/null
+++ b/src/core/DOMUtils.js
@@ -0,0 +1,36 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+export function updateTag(tagName, keyName, keyValue, attrName, attrValue) {
+ const node = document.head.querySelector(`${tagName}[${keyName}="${keyValue}"]`);
+ if (node && node.getAttribute(attrName) === attrValue) return;
+
+ // Remove and create a new tag in order to make it work with bookmarks in Safari
+ if (node) {
+ node.parentNode.removeChild(node);
+ }
+ if (typeof attrValue === 'string') {
+ const nextNode = document.createElement(tagName);
+ nextNode.setAttribute(keyName, keyValue);
+ nextNode.setAttribute(attrName, attrValue);
+ document.head.appendChild(nextNode);
+ }
+}
+
+export function updateMeta(name, content) {
+ updateTag('meta', 'name', name, 'content', content);
+}
+
+export function updateCustomMeta(property, content) {
+ updateTag('meta', 'property', property, 'content', content);
+}
+
+export function updateLink(rel, href) {
+ updateTag('link', 'rel', rel, 'href', href);
+}
diff --git a/src/core/devUtils.js b/src/core/devUtils.js
new file mode 100644
index 0000000..2d59ff5
--- /dev/null
+++ b/src/core/devUtils.js
@@ -0,0 +1,22 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+/* eslint-disable global-require */
+
+if (module.hot || __DEV__) {
+ module.exports = {
+ // The red box (aka red screen of death) component to display your errors
+ // https://github.com/commissure/redbox-react
+ ErrorReporter: require('redbox-react').default,
+
+ // Force-updates React component tree recursively
+ // https://github.com/gaearon/react-deep-force-update
+ deepForceUpdate: require('react-deep-force-update'),
+ };
+}
diff --git a/src/core/fetch/fetch.client.js b/src/core/fetch/fetch.client.js
new file mode 100644
index 0000000..5201119
--- /dev/null
+++ b/src/core/fetch/fetch.client.js
@@ -0,0 +1,15 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import 'whatwg-fetch';
+
+export default self.fetch.bind(self);
+export const Headers = self.Headers;
+export const Request = self.Request;
+export const Response = self.Response;
diff --git a/src/core/fetch/fetch.server.js b/src/core/fetch/fetch.server.js
new file mode 100644
index 0000000..991f9ed
--- /dev/null
+++ b/src/core/fetch/fetch.server.js
@@ -0,0 +1,33 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import Promise from 'bluebird';
+import fetch, { Request, Headers, Response } from 'node-fetch';
+import { host } from '../../config';
+
+fetch.Promise = Promise;
+Response.Promise = Promise;
+
+function localUrl(url) {
+ if (url.startsWith('//')) {
+ return `https:${url}`;
+ }
+
+ if (url.startsWith('http')) {
+ return url;
+ }
+
+ return `http://${host}${url}`;
+}
+
+function localFetch(url, options) {
+ return fetch(localUrl(url), options);
+}
+
+export { localFetch as default, Request, Headers, Response };
diff --git a/src/core/fetch/package.json b/src/core/fetch/package.json
new file mode 100644
index 0000000..a497e20
--- /dev/null
+++ b/src/core/fetch/package.json
@@ -0,0 +1,6 @@
+{
+ "private": true,
+ "name": "fetch",
+ "main": "./fetch.server.js",
+ "browser": "./fetch.client.js"
+}
diff --git a/src/core/history.js b/src/core/history.js
new file mode 100644
index 0000000..2f2faac
--- /dev/null
+++ b/src/core/history.js
@@ -0,0 +1,14 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import createBrowserHistory from 'history/createBrowserHistory';
+
+// Navigation manager, e.g. history.push('/home')
+// https://github.com/mjackson/history
+export default process.env.BROWSER && createBrowserHistory();
diff --git a/src/reducers/index.js b/src/reducers/index.js
new file mode 100644
index 0000000..d0495ad
--- /dev/null
+++ b/src/reducers/index.js
@@ -0,0 +1,6 @@
+import { combineReducers } from 'redux';
+import runtime from './runtime';
+
+export default combineReducers({
+ runtime,
+});
diff --git a/src/reducers/runtime.js b/src/reducers/runtime.js
new file mode 100644
index 0000000..f5f2584
--- /dev/null
+++ b/src/reducers/runtime.js
@@ -0,0 +1,13 @@
+import { SET_RUNTIME_VARIABLE } from '../constants';
+
+export default function runtime(state = {}, action) {
+ switch (action.type) {
+ case SET_RUNTIME_VARIABLE:
+ return {
+ ...state,
+ [action.payload.name]: action.payload.value,
+ };
+ default:
+ return state;
+ }
+}
diff --git a/src/routes/author/Author.js b/src/routes/author/Author.js
new file mode 100644
index 0000000..ea77f64
--- /dev/null
+++ b/src/routes/author/Author.js
@@ -0,0 +1,35 @@
+import React, { Component } from 'react';
+import Author from '../../components/Author';
+
+export class AuthorContainer extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = this.getInitialState();
+ }
+
+ fetchData() {
+ const user = this.props.author;
+ const repo = 'nodejs/node';
+ const count = 10;
+
+ fetch(`https://api.github.com/repos/${repo}/commits?per_page=${count}&author=${user}`)
+ .then((response) => response.json())
+ .then((json) => this.setState({commits: json}));
+ }
+
+ getInitialState() {
+ return {
+ commits: []
+ };
+ }
+
+ componentDidMount() {
+ this.fetchData();
+ }
+
+ render() {
+ return ;
+ }
+}
+export default AuthorContainer;
diff --git a/src/routes/author/index.js b/src/routes/author/index.js
new file mode 100644
index 0000000..eb73da7
--- /dev/null
+++ b/src/routes/author/index.js
@@ -0,0 +1,27 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import Author from './Author';
+import Layout from '../../components/Layout';
+
+export default {
+
+ path: '/:author',
+
+ async action(context, { author })
+ {
+ return {
+ title: '',
+ component: ,
+ description: 'Some cool tool to see latest node commits',
+ };
+ },
+
+};
diff --git a/src/routes/commits/Commits.js b/src/routes/commits/Commits.js
new file mode 100644
index 0000000..f94108c
--- /dev/null
+++ b/src/routes/commits/Commits.js
@@ -0,0 +1,39 @@
+import React, { Component } from 'react';
+import fetch from '../../core/fetch'
+import Commits from '../../components/Commits';
+
+export class CommitsComponent extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = this.getInitialState();
+ }
+
+ fetchData() {
+ const repo = 'nodejs/node';
+ const count = 30;
+
+ fetch(`https://api.github.com/repos/${repo}/commits?per_page=${count}`)
+ .then((response) => response.json())
+ .then((json) => this.setState({commits: json}));
+ }
+
+ getInitialState() {
+ return {
+ commits: []
+ };
+ }
+
+ componentDidMount() {
+ this.fetchData();
+ }
+
+ render() {
+ return ;
+ }
+}
+
+export default CommitsComponent;
+
+
+
diff --git a/src/routes/commits/index.js b/src/routes/commits/index.js
new file mode 100644
index 0000000..0c50c23
--- /dev/null
+++ b/src/routes/commits/index.js
@@ -0,0 +1,26 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import Commits from './Commits';
+import Layout from '../../components/Layout';
+
+export default {
+
+ path: '/commits',
+
+ async action() {
+ return {
+ title: 'Commits',
+ component: ,
+ description: 'Some cool tool to see latest node commits',
+ };
+ },
+
+};
diff --git a/src/routes/error/ErrorPage.css b/src/routes/error/ErrorPage.css
new file mode 100644
index 0000000..5bfc88d
--- /dev/null
+++ b/src/routes/error/ErrorPage.css
@@ -0,0 +1,56 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+* {
+ line-height: 1.2;
+ margin: 0;
+}
+
+html {
+ color: #888;
+ display: table;
+ font-family: sans-serif;
+ height: 100%;
+ text-align: center;
+ width: 100%;
+}
+
+body {
+ display: table-cell;
+ vertical-align: middle;
+ padding: 2em;
+}
+
+h1 {
+ color: #555;
+ font-size: 2em;
+ font-weight: 400;
+}
+
+p {
+ margin: 0 auto;
+ width: 280px;
+}
+
+pre {
+ text-align: left;
+ margin-top: 2rem;
+}
+
+@media only screen and (max-width: 280px) {
+ body,
+ p {
+ width: 95%;
+ }
+
+ h1 {
+ font-size: 1.5em;
+ margin: 0 0 0.3em;
+ }
+}
diff --git a/src/routes/error/ErrorPage.js b/src/routes/error/ErrorPage.js
new file mode 100644
index 0000000..5c52614
--- /dev/null
+++ b/src/routes/error/ErrorPage.js
@@ -0,0 +1,45 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import React, { PropTypes } from 'react';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import s from './ErrorPage.css';
+
+class ErrorPage extends React.Component {
+ static propTypes = {
+ error: PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ message: PropTypes.string.isRequired,
+ stack: PropTypes.string.isRequired,
+ }).isRequired,
+ };
+
+ render() {
+ if (__DEV__) {
+ const { error } = this.props;
+ return (
+
+
{error.name}
+
{error.message}
+
{error.stack}
+
+ );
+ }
+
+ return (
+
+
Error
+
Sorry, a critical error occurred on this page.
+
+ );
+ }
+}
+
+export { ErrorPage as ErrorPageWithoutStyle };
+export default withStyles(s)(ErrorPage);
diff --git a/src/routes/error/index.js b/src/routes/error/index.js
new file mode 100644
index 0000000..159607f
--- /dev/null
+++ b/src/routes/error/index.js
@@ -0,0 +1,26 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import ErrorPage from './ErrorPage';
+
+export default {
+
+ path: '/error',
+
+ action({ error }) {
+ return {
+ title: error.name,
+ description: error.message,
+ component: ,
+ status: error.status || 500,
+ };
+ },
+
+};
diff --git a/src/routes/home/Home.js b/src/routes/home/Home.js
new file mode 100644
index 0000000..2730206
--- /dev/null
+++ b/src/routes/home/Home.js
@@ -0,0 +1,16 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import Main from '../../components/Main';
+
+export const HomeComponent = () =>
+ ;
+
+export default HomeComponent;
diff --git a/src/routes/home/Home.test.js b/src/routes/home/Home.test.js
new file mode 100644
index 0000000..265494f
--- /dev/null
+++ b/src/routes/home/Home.test.js
@@ -0,0 +1,21 @@
+/* eslint-env mocha */
+/* eslint-disable no-unused-expressions */
+
+import React from 'react';
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import { HomeComponent } from './Home';
+import Main from './../../components/Main';
+
+describe(`#File: ${__filename}`, () => {
+ it('should exist', () => {
+ expect(HomeComponent).to.be.ok;
+ });
+});
+
+describe('Rendering home component', () => {
+ const homeWrapper = shallow();
+ it('should render main component', () => {
+ expect(homeWrapper.find(Main)).to.have.length(1);
+ });
+});
diff --git a/src/routes/home/index.js b/src/routes/home/index.js
new file mode 100644
index 0000000..ea51d5b
--- /dev/null
+++ b/src/routes/home/index.js
@@ -0,0 +1,26 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import Home from './Home';
+import Layout from '../../components/Layout';
+
+export default {
+
+ path: '/',
+
+ async action() {
+ return {
+ title: 'HP',
+ component: ,
+ description: 'Home page',
+ };
+ },
+
+};
diff --git a/src/routes/index.js b/src/routes/index.js
new file mode 100644
index 0000000..21bf050
--- /dev/null
+++ b/src/routes/index.js
@@ -0,0 +1,38 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+/* eslint-disable global-require */
+
+// The top-level (parent) route
+export default {
+
+ path: '/',
+
+ // Keep in mind, routes are evaluated in order
+ children: [
+ require('./home').default,
+ require('./commits').default,
+ require('./author').default,
+
+ // Wildcard routes, e.g. { path: '*', ... } (must go last)
+ require('./notFound').default,
+ ],
+
+ async action({ next }) {
+ // Execute each child route until one of them return the result
+ const route = await next();
+
+ // Provide default values for title, description etc.
+ route.title = `${route.title || 'Commits app'}`;
+ route.description = route.description || '';
+
+ return route;
+ },
+
+};
diff --git a/src/routes/notFound/NotFound.css b/src/routes/notFound/NotFound.css
new file mode 100644
index 0000000..feef11a
--- /dev/null
+++ b/src/routes/notFound/NotFound.css
@@ -0,0 +1,21 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+@import '../../components/variables.css';
+
+.root {
+ padding-left: 20px;
+ padding-right: 20px;
+}
+
+.container {
+ margin: 0 auto;
+ padding: 0 0 40px;
+ max-width: var(--max-content-width);
+}
diff --git a/src/routes/notFound/NotFound.js b/src/routes/notFound/NotFound.js
new file mode 100644
index 0000000..857fc50
--- /dev/null
+++ b/src/routes/notFound/NotFound.js
@@ -0,0 +1,31 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import React, { PropTypes } from 'react';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import s from './NotFound.css';
+
+class NotFound extends React.Component {
+ static propTypes = {
+ title: PropTypes.string.isRequired,
+ };
+
+ render() {
+ return (
+
+
+
{this.props.title}
+
Sorry, the page you were trying to view does not exist.
+
+
+ );
+ }
+}
+
+export default withStyles(s)(NotFound);
diff --git a/src/routes/notFound/index.js b/src/routes/notFound/index.js
new file mode 100644
index 0000000..3a4034e
--- /dev/null
+++ b/src/routes/notFound/index.js
@@ -0,0 +1,28 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import Layout from '../../components/Layout';
+import NotFound from './NotFound';
+
+const title = 'Page Not Found';
+
+export default {
+
+ path: '*',
+
+ action() {
+ return {
+ title,
+ component: ,
+ status: 404,
+ };
+ },
+
+};
diff --git a/src/server.js b/src/server.js
new file mode 100644
index 0000000..8b3f56d
--- /dev/null
+++ b/src/server.js
@@ -0,0 +1,144 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import path from 'path';
+import express from 'express';
+import cookieParser from 'cookie-parser';
+import bodyParser from 'body-parser';
+import React from 'react';
+import ReactDOM from 'react-dom/server';
+import UniversalRouter from 'universal-router';
+import PrettyError from 'pretty-error';
+import App from './components/App';
+import Html from './components/Html';
+import { ErrorPageWithoutStyle } from './routes/error/ErrorPage';
+import errorPageStyle from './routes/error/ErrorPage.css';
+import routes from './routes';
+import assets from './assets.json'; // eslint-disable-line import/no-unresolved
+import configureStore from './store/configureStore';
+import { setRuntimeVariable } from './actions/runtime';
+import { port } from './config';
+
+const app = express();
+
+//
+// Tell any CSS tooling (such as Material UI) to use all vendor prefixes if the
+// user agent is not known.
+// -----------------------------------------------------------------------------
+global.navigator = global.navigator || {};
+global.navigator.userAgent = global.navigator.userAgent || 'all';
+
+//
+// Register Node.js middleware
+// -----------------------------------------------------------------------------
+app.use(express.static(path.join(__dirname, 'public')));
+app.use(cookieParser());
+app.use(bodyParser.urlencoded({ extended: true }));
+app.use(bodyParser.json());
+
+
+if (__DEV__) {
+ app.enable('trust proxy');
+}
+
+//
+// Register server-side rendering middleware
+// -----------------------------------------------------------------------------
+app.get('*', async (req, res, next) => {
+ try {
+ const store = configureStore({
+ user: req.user || null,
+ }, {
+ cookie: req.headers.cookie,
+ });
+
+ store.dispatch(setRuntimeVariable({
+ name: 'initialNow',
+ value: Date.now(),
+ }));
+
+ const css = new Set();
+
+ // Global (context) variables that can be easily accessed from any React component
+ // https://facebook.github.io/react/docs/context.html
+ const context = {
+ // Enables critical path CSS rendering
+ // https://github.com/kriasoft/isomorphic-style-loader
+ insertCss: (...styles) => {
+ // eslint-disable-next-line no-underscore-dangle
+ styles.forEach(style => css.add(style._getCss()));
+ },
+ // Initialize a new Redux store
+ // http://redux.js.org/docs/basics/UsageWithReact.html
+ store,
+ };
+
+ const route = await UniversalRouter.resolve(routes, {
+ ...context,
+ path: req.path,
+ query: req.query,
+ });
+
+ if (route.redirect) {
+ res.redirect(route.status || 302, route.redirect);
+ return;
+ }
+
+ const data = { ...route };
+ data.children = ReactDOM.renderToString({route.component});
+ data.styles = [
+ { id: 'css', cssText: [...css].join('') },
+ ];
+ data.scripts = [
+ assets.vendor.js,
+ assets.client.js,
+ ];
+ data.state = context.store.getState();
+ if (assets[route.chunk]) {
+ data.scripts.push(assets[route.chunk].js);
+ }
+
+ const html = ReactDOM.renderToStaticMarkup();
+ res.status(route.status || 200);
+ res.send(`${html}`);
+ } catch (err) {
+ next(err);
+ }
+});
+
+//
+// Error handling
+// -----------------------------------------------------------------------------
+const pe = new PrettyError();
+pe.skipNodeFiles();
+pe.skipPackage('express');
+
+app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars
+ console.log(pe.render(err)); // eslint-disable-line no-console
+ const html = ReactDOM.renderToStaticMarkup(
+
+ {ReactDOM.renderToString()}
+ ,
+ );
+ res.status(err.status || 500);
+ res.send(`${html}`);
+});
+
+//
+// Launch the server
+// -----------------------------------------------------------------------------
+/* eslint-disable no-console */
+app.listen(port, () => {
+ console.log(`The server is running at http://localhost:${port}/`);
+});
+/* eslint-enable no-console */
diff --git a/src/store/configureStore.js b/src/store/configureStore.js
new file mode 100644
index 0000000..87f7b8d
--- /dev/null
+++ b/src/store/configureStore.js
@@ -0,0 +1,42 @@
+import { createStore, applyMiddleware, compose } from 'redux';
+import thunk from 'redux-thunk';
+import rootReducer from '../reducers';
+import createHelpers from './createHelpers';
+import createLogger from './logger';
+
+export default function configureStore(initialState, helpersConfig) {
+ const helpers = createHelpers(helpersConfig);
+ const middleware = [thunk.withExtraArgument(helpers)];
+
+ let enhancer;
+
+ if (__DEV__) {
+ middleware.push(createLogger());
+
+ // https://github.com/zalmoxisus/redux-devtools-extension#redux-devtools-extension
+ let devToolsExtension = f => f;
+ if (process.env.BROWSER && window.devToolsExtension) {
+ devToolsExtension = window.devToolsExtension();
+ }
+
+ enhancer = compose(
+ applyMiddleware(...middleware),
+ devToolsExtension,
+ );
+ } else {
+ enhancer = applyMiddleware(...middleware);
+ }
+
+ // See https://github.com/rackt/redux/releases/tag/v3.1.0
+ const store = createStore(rootReducer, initialState, enhancer);
+
+ // Hot reload reducers (requires Webpack or Browserify HMR to be enabled)
+ if (__DEV__ && module.hot) {
+ module.hot.accept('../reducers', () =>
+ // eslint-disable-next-line global-require
+ store.replaceReducer(require('../reducers').default),
+ );
+ }
+
+ return store;
+}
diff --git a/src/store/createHelpers.js b/src/store/createHelpers.js
new file mode 100644
index 0000000..79f6304
--- /dev/null
+++ b/src/store/createHelpers.js
@@ -0,0 +1,32 @@
+import fetch from '../core/fetch';
+
+function createFetchKnowingCookie({ cookie }) {
+ if (!process.env.BROWSER) {
+ return (url, options = {}) => {
+ const isLocalUrl = /^\/($|[^/])/.test(url);
+
+ // pass cookie only for itself.
+ // We can't know cookies for other sites BTW
+ if (isLocalUrl && options.credentials === 'include') {
+ const headers = {
+ ...options.headers,
+ cookie,
+ };
+ return fetch(url, { ...options, headers });
+ }
+
+ return fetch(url, options);
+ };
+ }
+
+ return fetch;
+}
+
+export default function createHelpers(config) {
+ const fetchKnowingCookie = createFetchKnowingCookie(config);
+
+ return {
+ fetch: fetchKnowingCookie,
+ history: config.history,
+ };
+}
diff --git a/src/store/logger/logger.client.js b/src/store/logger/logger.client.js
new file mode 100644
index 0000000..b413011
--- /dev/null
+++ b/src/store/logger/logger.client.js
@@ -0,0 +1,7 @@
+import reduxLogger from 'redux-logger';
+
+export default function createLogger() {
+ return reduxLogger({
+ collapsed: true,
+ });
+}
diff --git a/src/store/logger/logger.server.js b/src/store/logger/logger.server.js
new file mode 100644
index 0000000..750060d
--- /dev/null
+++ b/src/store/logger/logger.server.js
@@ -0,0 +1,13 @@
+import { inspect } from 'util';
+
+// Server side redux action logger
+export default function createLogger() {
+ // eslint-disable-next-line no-unused-vars
+ return store => next => (action) => {
+ const formattedPayload = inspect(action.payload, {
+ colors: true,
+ });
+ console.log(` * ${action.type}: ${formattedPayload}`); // eslint-disable-line no-console
+ return next(action);
+ };
+}
diff --git a/src/store/logger/package.json b/src/store/logger/package.json
new file mode 100644
index 0000000..13a7ca3
--- /dev/null
+++ b/src/store/logger/package.json
@@ -0,0 +1,6 @@
+{
+ "private": true,
+ "name": "redux-logger-middleware",
+ "main": "./logger.server.js",
+ "browser": "./logger.client.js"
+}
diff --git a/test/.eslintrc b/test/.eslintrc
new file mode 100644
index 0000000..499c2d6
--- /dev/null
+++ b/test/.eslintrc
@@ -0,0 +1,9 @@
+{
+ "env": {
+ "mocha": true
+ },
+ "rules": {
+ "no-unused-expressions": 0,
+ "padded-blocks": 0
+ }
+}
diff --git a/test/setup.js b/test/setup.js
new file mode 100644
index 0000000..0701dd2
--- /dev/null
+++ b/test/setup.js
@@ -0,0 +1,25 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+/* Configure Mocha test runner, see package.json/scripts/test */
+
+process.env.NODE_ENV = 'test';
+
+function noop() {
+ return null;
+}
+
+require.extensions['.css'] = noop;
+require.extensions['.scss'] = noop;
+require.extensions['.md'] = noop;
+require.extensions['.png'] = noop;
+require.extensions['.svg'] = noop;
+require.extensions['.jpg'] = noop;
+require.extensions['.jpeg'] = noop;
+require.extensions['.gif'] = noop;
diff --git a/tools/.eslintrc b/tools/.eslintrc
new file mode 100644
index 0000000..42d2e91
--- /dev/null
+++ b/tools/.eslintrc
@@ -0,0 +1,6 @@
+{
+ "rules": {
+ "no-console": "off",
+ "global-require": "off"
+ }
+}
diff --git a/tools/README.md b/tools/README.md
new file mode 100644
index 0000000..bb4d97f
--- /dev/null
+++ b/tools/README.md
@@ -0,0 +1,50 @@
+## Build Automation Tools
+
+##### `yarn start` (`start.js`)
+
+* Cleans up the output `/build` directory (`clean.js`)
+* Copies static files to the output folder (`copy.js`)
+* Launches [Webpack](https://webpack.github.io/) compiler in a watch mode (via [webpack-middleware](https://github.com/kriasoft/webpack-middleware))
+* Launches Node.js server from the compiled output folder (`runServer.js`)
+* Launches [Browsersync](https://browsersync.io/),
+ [Hot Module Replacement](https://webpack.github.io/docs/hot-module-replacement), and
+ [React Hot Loader](https://github.com/gaearon/react-hot-loader)
+
+##### `yarn run build` (`build.js`)
+
+* Cleans up the output `/build` folder (`clean.js`)
+* Copies static files to the output folder (`copy.js`)
+* Creates application bundles with Webpack (`bundle.js`, `webpack.config.js`)
+
+##### `yarn run deploy` (`deploy.js`)
+
+* Builds the project from source files (`build.js`)
+* Pushes the contents of the `/build` folder to a remote server with Git
+
+##### Options
+
+Flag | Description
+----------- | --------------------------------------------------
+`--release` | Minimizes and optimizes the compiled output
+`--verbose` | Prints detailed information to the console
+`--static` | Renders [specified routes](./render.js#L15) as static html files
+`--docker` | Build an image from a Dockerfile
+
+For example:
+
+```sh
+$ yarn run build -- --release --verbose # Build the app in production mode
+```
+
+or
+
+```sh
+$ yarn start -- --release # Launch dev server in production mode
+```
+
+#### Misc
+
+* `webpack.config.js` - Webpack configuration for both client-side and server-side bundles
+* `postcss.config.js` - PostCSS configuration for transforming styles with JS plugins
+* `run.js` - Helps to launch other scripts with `babel-node` (e.g. `babel-node tools/run build`)
+* `.eslintrc` - ESLint overrides for built automation scripts
diff --git a/tools/build.js b/tools/build.js
new file mode 100644
index 0000000..1d562c3
--- /dev/null
+++ b/tools/build.js
@@ -0,0 +1,36 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import cp from 'child_process';
+import run from './run';
+import clean from './clean';
+import copy from './copy';
+import bundle from './bundle';
+import render from './render';
+import pkg from '../package.json';
+
+/**
+ * Compiles the project from source files into a distributable
+ * format and copies it to the output (build) folder.
+ */
+async function build() {
+ await run(clean);
+ await run(copy);
+ await run(bundle);
+
+ if (process.argv.includes('--static')) {
+ await run(render);
+ }
+
+ if (process.argv.includes('--docker')) {
+ cp.spawnSync('docker', ['build', '-t', pkg.name, '.'], { stdio: 'inherit' });
+ }
+}
+
+export default build;
diff --git a/tools/bundle.js b/tools/bundle.js
new file mode 100644
index 0000000..2d0047b
--- /dev/null
+++ b/tools/bundle.js
@@ -0,0 +1,29 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import webpack from 'webpack';
+import webpackConfig from './webpack.config';
+
+/**
+ * Creates application bundles from the source files.
+ */
+function bundle() {
+ return new Promise((resolve, reject) => {
+ webpack(webpackConfig).run((err, stats) => {
+ if (err) {
+ return reject(err);
+ }
+
+ console.log(stats.toString(webpackConfig[0].stats));
+ return resolve();
+ });
+ });
+}
+
+export default bundle;
diff --git a/tools/clean.js b/tools/clean.js
new file mode 100644
index 0000000..f2c6055
--- /dev/null
+++ b/tools/clean.js
@@ -0,0 +1,31 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import { cleanDir } from './lib/fs';
+
+/**
+ * Cleans up the output (build) directory.
+ */
+function clean() {
+ return Promise.all([
+ cleanDir('build/*', {
+ nosort: true,
+ dot: true,
+ ignore: ['build/.git', 'build/public'],
+ }),
+
+ cleanDir('build/public/*', {
+ nosort: true,
+ dot: true,
+ ignore: ['build/public/.git'],
+ }),
+ ]);
+}
+
+export default clean;
diff --git a/tools/copy.js b/tools/copy.js
new file mode 100644
index 0000000..8bc9000
--- /dev/null
+++ b/tools/copy.js
@@ -0,0 +1,64 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import path from 'path';
+import chokidar from 'chokidar';
+import { writeFile, copyFile, makeDir, copyDir, cleanDir } from './lib/fs';
+import pkg from '../package.json';
+import { format } from './run';
+
+/**
+ * Copies static files such as robots.txt, favicon.ico to the
+ * output (build) folder.
+ */
+async function copy() {
+ await makeDir('build');
+ await Promise.all([
+ writeFile('build/package.json', JSON.stringify({
+ private: true,
+ engines: pkg.engines,
+ dependencies: pkg.dependencies,
+ scripts: {
+ start: 'node server.js',
+ },
+ }, null, 2)),
+ copyFile('LICENSE.txt', 'build/LICENSE.txt'),
+ copyDir('public', 'build/public'),
+ ]);
+
+ if (process.argv.includes('--watch')) {
+ const watcher = chokidar.watch([
+ 'public/**/*',
+ ], { ignoreInitial: true });
+
+ watcher.on('all', async (event, filePath) => {
+ const start = new Date();
+ const src = path.relative('./', filePath);
+ const dist = path.join('build/', src.startsWith('src') ? path.relative('src', src) : src);
+ switch (event) {
+ case 'add':
+ case 'change':
+ await makeDir(path.dirname(dist));
+ await copyFile(filePath, dist);
+ break;
+ case 'unlink':
+ case 'unlinkDir':
+ cleanDir(dist, { nosort: true, dot: true });
+ break;
+ default:
+ return;
+ }
+ const end = new Date();
+ const time = end.getTime() - start.getTime();
+ console.log(`[${format(end)}] ${event} '${dist}' after ${time} ms`);
+ });
+ }
+}
+
+export default copy;
diff --git a/tools/deploy.js b/tools/deploy.js
new file mode 100644
index 0000000..5c609ce
--- /dev/null
+++ b/tools/deploy.js
@@ -0,0 +1,86 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import path from 'path';
+import fetch from 'node-fetch';
+import { spawn } from './lib/cp';
+import { makeDir } from './lib/fs';
+import run from './run';
+
+// GitHub Pages
+const remote = {
+ name: 'github',
+ url: 'https://github.com//.git',
+ branch: 'gh-pages',
+ website: 'https://.github.io//',
+ static: true,
+};
+
+// Azure Web Apps
+// const remote = {
+// name: 'azure',
+// url: 'https://@.scm.azurewebsites.net:443/.git',
+// branch: 'master',
+// website: `http://.azurewebsites.net`,
+// };
+
+const options = {
+ cwd: path.resolve(__dirname, '../build', remote.static ? 'public' : ''),
+ stdio: ['ignore', 'inherit', 'inherit'],
+};
+
+/**
+ * Deploy the contents of the `/build` folder to a remote server via Git.
+ */
+async function deploy() {
+ // Initialize a new repository
+ await makeDir('build/public');
+ await spawn('git', ['init', '--quiet'], options);
+
+ // Changing a remote's URL
+ let isRemoteExists = false;
+ try {
+ await spawn('git', ['config', '--get', `remote.${remote.name}.url`], options);
+ isRemoteExists = true;
+ } catch (error) {
+ /* skip */
+ }
+ await spawn('git', ['remote', isRemoteExists ? 'set-url' : 'add', remote.name, remote.url], options);
+
+ // Fetch the remote repository if it exists
+ let isRefExists = false;
+ try {
+ await spawn('git', ['ls-remote', '--exit-code', remote.url, remote.branch], options);
+ isRefExists = true;
+ } catch (error) {
+ /* skip */
+ }
+ if (isRefExists) {
+ await spawn('git', ['fetch', remote.name], options);
+ await spawn('git', ['reset', `${remote.name}/${remote.branch}`, '--hard'], options);
+ await spawn('git', ['clean', '--force'], options);
+ }
+
+ // Build the project in RELEASE mode which
+ // generates optimized and minimized bundles
+ process.argv.push('--release');
+ if (remote.static) process.argv.push('--static');
+ await run(require('./build').default);
+
+ // Push the contents of the build folder to the remote server via Git
+ await spawn('git', ['add', '.', '--all'], options);
+ await spawn('git', ['commit', '--message', `Update ${new Date().toISOString()}`], options);
+ await spawn('git', ['push', remote.name, `master:${remote.branch}`, '--force', '--set-upstream'], options);
+
+ // Check if the site was successfully deployed
+ const response = await fetch(remote.website);
+ console.log(`${remote.website} => ${response.status} ${response.statusText}`);
+}
+
+export default deploy;
diff --git a/tools/lib/cp.js b/tools/lib/cp.js
new file mode 100644
index 0000000..3cce0b7
--- /dev/null
+++ b/tools/lib/cp.js
@@ -0,0 +1,22 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import cp from 'child_process';
+
+export const spawn = (command, args, options) => new Promise((resolve, reject) => {
+ cp.spawn(command, args, options).on('close', (code) => {
+ if (code === 0) {
+ resolve();
+ } else {
+ reject(new Error(`${command} ${args.join(' ')} => ${code} (error)`));
+ }
+ });
+});
+
+export default { spawn };
diff --git a/tools/lib/fs.js b/tools/lib/fs.js
new file mode 100644
index 0000000..ec4971e
--- /dev/null
+++ b/tools/lib/fs.js
@@ -0,0 +1,79 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import fs from 'fs';
+import path from 'path';
+import glob from 'glob';
+import mkdirp from 'mkdirp';
+import rimraf from 'rimraf';
+
+export const readFile = file => new Promise((resolve, reject) => {
+ fs.readFile(file, 'utf8', (err, data) => (err ? reject(err) : resolve(data)));
+});
+
+export const writeFile = (file, contents) => new Promise((resolve, reject) => {
+ fs.writeFile(file, contents, 'utf8', err => (err ? reject(err) : resolve()));
+});
+
+export const copyFile = (source, target) => new Promise((resolve, reject) => {
+ let cbCalled = false;
+ function done(err) {
+ if (!cbCalled) {
+ cbCalled = true;
+ if (err) {
+ reject(err);
+ } else {
+ resolve();
+ }
+ }
+ }
+
+ const rd = fs.createReadStream(source);
+ rd.on('error', err => done(err));
+ const wr = fs.createWriteStream(target);
+ wr.on('error', err => done(err));
+ wr.on('close', err => done(err));
+ rd.pipe(wr);
+});
+
+export const readDir = (pattern, options) => new Promise((resolve, reject) =>
+ glob(pattern, options, (err, result) => (err ? reject(err) : resolve(result))),
+);
+
+export const makeDir = name => new Promise((resolve, reject) => {
+ mkdirp(name, err => (err ? reject(err) : resolve()));
+});
+
+export const copyDir = async (source, target) => {
+ const dirs = await readDir('**/*.*', {
+ cwd: source,
+ nosort: true,
+ dot: true,
+ });
+ await Promise.all(dirs.map(async (dir) => {
+ const from = path.resolve(source, dir);
+ const to = path.resolve(target, dir);
+ await makeDir(path.dirname(to));
+ await copyFile(from, to);
+ }));
+};
+
+export const cleanDir = (pattern, options) => new Promise((resolve, reject) =>
+ rimraf(pattern, { glob: options }, (err, result) => (err ? reject(err) : resolve(result))),
+);
+
+export default {
+ readFile,
+ writeFile,
+ copyFile,
+ readDir,
+ makeDir,
+ copyDir,
+ cleanDir,
+};
diff --git a/tools/lib/markdown-loader.js b/tools/lib/markdown-loader.js
new file mode 100644
index 0000000..15f7b3d
--- /dev/null
+++ b/tools/lib/markdown-loader.js
@@ -0,0 +1,23 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+const MarkdownIt = require('markdown-it');
+const fm = require('front-matter');
+
+module.exports = function markdownLoader(source) {
+ const md = new MarkdownIt({
+ html: true,
+ linkify: true,
+ });
+
+ const frontmatter = fm(source);
+ frontmatter.attributes.html = md.render(frontmatter.body);
+
+ return `module.exports = ${JSON.stringify(frontmatter.attributes)};`;
+};
diff --git a/tools/postcss.config.js b/tools/postcss.config.js
new file mode 100644
index 0000000..75eb477
--- /dev/null
+++ b/tools/postcss.config.js
@@ -0,0 +1,63 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+module.exports = () => ({
+ // The list of plugins for PostCSS
+ // https://github.com/postcss/postcss
+ plugins: [
+ // Transfer @import rule by inlining content, e.g. @import 'normalize.css'
+ // https://github.com/jonathantneal/postcss-partial-import
+ require('postcss-partial-import')(),
+ // Allow you to fix url() according to postcss to and/or from options
+ // https://github.com/postcss/postcss-url
+ require('postcss-url')(),
+ // W3C variables, e.g. :root { --color: red; } div { background: var(--color); }
+ // https://github.com/postcss/postcss-custom-properties
+ require('postcss-custom-properties')(),
+ // W3C CSS Custom Media Queries, e.g. @custom-media --small-viewport (max-width: 30em);
+ // https://github.com/postcss/postcss-custom-media
+ require('postcss-custom-media')(),
+ // CSS4 Media Queries, e.g. @media screen and (width >= 500px) and (width <= 1200px) { }
+ // https://github.com/postcss/postcss-media-minmax
+ require('postcss-media-minmax')(),
+ // W3C CSS Custom Selectors, e.g. @custom-selector :--heading h1, h2, h3, h4, h5, h6;
+ // https://github.com/postcss/postcss-custom-selectors
+ require('postcss-custom-selectors')(),
+ // W3C calc() function, e.g. div { height: calc(100px - 2em); }
+ // https://github.com/postcss/postcss-calc
+ require('postcss-calc')(),
+ // Allows you to nest one style rule inside another
+ // https://github.com/jonathantneal/postcss-nesting
+ require('postcss-nesting')(),
+ // Unwraps nested rules like how Sass does it
+ // https://github.com/postcss/postcss-nested
+ require('postcss-nested')(),
+ // W3C color() function, e.g. div { background: color(red alpha(90%)); }
+ // https://github.com/postcss/postcss-color-function
+ require('postcss-color-function')(),
+ // Convert CSS shorthand filters to SVG equivalent, e.g. .blur { filter: blur(4px); }
+ // https://github.com/iamvdo/pleeease-filters
+ require('pleeease-filters')(),
+ // Generate pixel fallback for "rem" units, e.g. div { margin: 2.5rem 2px 3em 100%; }
+ // https://github.com/robwierzbowski/node-pixrem
+ require('pixrem')(),
+ // W3C CSS Level4 :matches() pseudo class, e.g. p:matches(:first-child, .special) { }
+ // https://github.com/postcss/postcss-selector-matches
+ require('postcss-selector-matches')(),
+ // Transforms :not() W3C CSS Level 4 pseudo class to :not() CSS Level 3 selectors
+ // https://github.com/postcss/postcss-selector-not
+ require('postcss-selector-not')(),
+ // Postcss flexbox bug fixer
+ // https://github.com/luisrudge/postcss-flexbugs-fixes
+ require('postcss-flexbugs-fixes')(),
+ // Add vendor prefixes to CSS rules using values from caniuse.com
+ // https://github.com/postcss/autoprefixer
+ require('autoprefixer')(/* package.json/browserslist */),
+ ],
+});
diff --git a/tools/render.js b/tools/render.js
new file mode 100644
index 0000000..1acce21
--- /dev/null
+++ b/tools/render.js
@@ -0,0 +1,62 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import path from 'path';
+import fetch from 'node-fetch';
+import { writeFile, makeDir } from './lib/fs';
+import runServer from './runServer';
+
+// Enter your paths here which you want to render as static
+// Example:
+// const routes = [
+// '/', // => build/public/index.html
+// '/page', // => build/public/page.html
+// '/page/', // => build/public/page/index.html
+// '/page/name', // => build/public/page/name.html
+// '/page/name/', // => build/public/page/name/index.html
+// ];
+const routes = [
+ '/',
+ '/contact',
+ '/login',
+ '/register',
+ '/about',
+ '/privacy',
+ '/404', // https://help.github.com/articles/creating-a-custom-404-page-for-your-github-pages-site/
+];
+
+async function render() {
+ const server = await runServer();
+
+ // add dynamic routes
+ // const products = await fetch(`http://${server.host}/api/products`).then(res => res.json());
+ // products.forEach(product => routes.push(
+ // `/product/${product.uri}`,
+ // `/product/${product.uri}/specs`
+ // ));
+
+ await Promise.all(routes.map(async (route, index) => {
+ const url = `http://${server.host}${route}`;
+ const fileName = route.endsWith('/') ? 'index.html' : `${path.basename(route, '.html')}.html`;
+ const dirName = path.join('build/public', route.endsWith('/') ? route : path.dirname(route));
+ const dist = path.join(dirName, fileName);
+ const timeStart = new Date();
+ const response = await fetch(url);
+ const timeEnd = new Date();
+ const text = await response.text();
+ await makeDir(dirName);
+ await writeFile(dist, text);
+ const time = timeEnd.getTime() - timeStart.getTime();
+ console.log(`#${index + 1} ${dist} => ${response.status} ${response.statusText} (${time} ms)`);
+ }));
+
+ server.kill('SIGTERM');
+}
+
+export default render;
diff --git a/tools/run.js b/tools/run.js
new file mode 100644
index 0000000..b695e65
--- /dev/null
+++ b/tools/run.js
@@ -0,0 +1,36 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+export function format(time) {
+ return time.toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, '$1');
+}
+
+function run(fn, options) {
+ const task = typeof fn.default === 'undefined' ? fn : fn.default;
+ const start = new Date();
+ console.log(
+ `[${format(start)}] Starting '${task.name}${options ? ` (${options})` : ''}'...`,
+ );
+ return task(options).then((resolution) => {
+ const end = new Date();
+ const time = end.getTime() - start.getTime();
+ console.log(
+ `[${format(end)}] Finished '${task.name}${options ? ` (${options})` : ''}' after ${time} ms`,
+ );
+ return resolution;
+ });
+}
+
+if (require.main === module && process.argv.length > 2) {
+ delete require.cache[__filename]; // eslint-disable-line no-underscore-dangle
+ const module = require(`./${process.argv[2]}.js`).default; // eslint-disable-line import/no-dynamic-require
+ run(module).catch((err) => { console.error(err.stack); process.exit(1); });
+}
+
+export default run;
diff --git a/tools/runServer.js b/tools/runServer.js
new file mode 100644
index 0000000..9c9dfb9
--- /dev/null
+++ b/tools/runServer.js
@@ -0,0 +1,71 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import path from 'path';
+import cp from 'child_process';
+import webpackConfig from './webpack.config';
+
+// Should match the text string used in `src/server.js/server.listen(...)`
+const RUNNING_REGEXP = /The server is running at http:\/\/(.*?)\//;
+
+let server;
+let pending = true;
+const [, serverConfig] = webpackConfig;
+const serverPath = path.join(serverConfig.output.path, serverConfig.output.filename);
+
+// Launch or restart the Node.js server
+function runServer() {
+ return new Promise((resolve) => {
+ function onStdOut(data) {
+ const time = new Date().toTimeString();
+ const match = data.toString('utf8').match(RUNNING_REGEXP);
+
+ process.stdout.write(time.replace(/.*(\d{2}:\d{2}:\d{2}).*/, '[$1] '));
+ process.stdout.write(data);
+
+ if (match) {
+ server.host = match[1];
+ server.stdout.removeListener('data', onStdOut);
+ server.stdout.on('data', x => process.stdout.write(x));
+ pending = false;
+ resolve(server);
+ }
+ }
+
+ if (server) {
+ server.kill('SIGTERM');
+ }
+
+ server = cp.spawn('node', [serverPath], {
+ env: Object.assign({ NODE_ENV: 'development' }, process.env),
+ silent: false,
+ });
+
+ if (pending) {
+ server.once('exit', (code, signal) => {
+ if (pending) {
+ throw new Error(`Server terminated unexpectedly with code: ${code} signal: ${signal}`);
+ }
+ });
+ }
+
+ server.stdout.on('data', onStdOut);
+ server.stderr.on('data', x => process.stderr.write(x));
+
+ return server;
+ });
+}
+
+process.on('exit', () => {
+ if (server) {
+ server.kill('SIGTERM');
+ }
+});
+
+export default runServer;
diff --git a/tools/start.js b/tools/start.js
new file mode 100644
index 0000000..a250787
--- /dev/null
+++ b/tools/start.js
@@ -0,0 +1,90 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import browserSync from 'browser-sync';
+import webpack from 'webpack';
+import webpackDevMiddleware from 'webpack-dev-middleware';
+import webpackHotMiddleware from 'webpack-hot-middleware';
+import WriteFilePlugin from 'write-file-webpack-plugin';
+import run from './run';
+import runServer from './runServer';
+import webpackConfig from './webpack.config';
+import clean from './clean';
+import copy from './copy';
+
+const isDebug = !process.argv.includes('--release');
+process.argv.push('--watch');
+
+const [clientConfig, serverConfig] = webpackConfig;
+
+/**
+ * Launches a development web server with "live reload" functionality -
+ * synchronizing URLs, interactions and code changes across multiple devices.
+ */
+async function start() {
+ await run(clean);
+ await run(copy);
+ await new Promise((resolve) => {
+ // Save the server-side bundle files to the file system after compilation
+ // https://github.com/webpack/webpack-dev-server/issues/62
+ serverConfig.plugins.push(new WriteFilePlugin({ log: false }));
+
+ // Hot Module Replacement (HMR) + React Hot Reload
+ if (isDebug) {
+ clientConfig.entry.client = [...new Set([
+ 'babel-polyfill',
+ 'react-hot-loader/patch',
+ 'webpack-hot-middleware/client',
+ ].concat(clientConfig.entry.client))];
+ clientConfig.output.filename = clientConfig.output.filename.replace('[chunkhash', '[hash');
+ clientConfig.output.chunkFilename = clientConfig.output.chunkFilename.replace('[chunkhash', '[hash');
+ const { query } = clientConfig.module.rules.find(x => x.loader === 'babel-loader');
+ query.plugins = ['react-hot-loader/babel'].concat(query.plugins || []);
+ clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
+ clientConfig.plugins.push(new webpack.NoEmitOnErrorsPlugin());
+ }
+
+ const bundler = webpack(webpackConfig);
+ const wpMiddleware = webpackDevMiddleware(bundler, {
+ // IMPORTANT: webpack middleware can't access config,
+ // so we should provide publicPath by ourselves
+ publicPath: clientConfig.output.publicPath,
+
+ // Pretty colored output
+ stats: clientConfig.stats,
+
+ // For other settings see
+ // https://webpack.github.io/docs/webpack-dev-middleware
+ });
+ const hotMiddleware = webpackHotMiddleware(bundler.compilers[0]);
+
+ let handleBundleComplete = async () => {
+ handleBundleComplete = stats => !stats.stats[1].compilation.errors.length && runServer();
+
+ const server = await runServer();
+ const bs = browserSync.create();
+
+ bs.init({
+ ...isDebug ? {} : { notify: false, ui: false },
+
+ proxy: {
+ target: server.host,
+ middleware: [wpMiddleware, hotMiddleware],
+ proxyOptions: {
+ xfwd: true,
+ },
+ },
+ }, resolve);
+ };
+
+ bundler.plugin('done', stats => handleBundleComplete(stats));
+ });
+}
+
+export default start;
diff --git a/tools/webpack.config.js b/tools/webpack.config.js
new file mode 100644
index 0000000..c438a98
--- /dev/null
+++ b/tools/webpack.config.js
@@ -0,0 +1,363 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-present Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import path from 'path';
+import webpack from 'webpack';
+import AssetsPlugin from 'assets-webpack-plugin';
+import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
+import pkg from '../package.json';
+
+const isDebug = !process.argv.includes('--release');
+const isVerbose = process.argv.includes('--verbose');
+const isAnalyse = process.argv.includes('--analyse') || process.argv.includes('--analyze');
+const port = parseInt(process.env.PORT || '3000', 10);
+const analyzerPort = port + 3;
+
+// Can be `server`, `static` or `disabled`.
+// In `server` mode analyzer will start HTTP server to show bundle report.
+// In `static` mode single HTML file with bundle report will be generated.
+// In `disabled` mode you can use this plugin to just generate Webpack Stats JSON
+// file by setting `generateStatsFile` to `true`.
+let analyzerMode = 'disabled';
+if (isAnalyse) {
+ analyzerMode = 'server';
+} else if (!isDebug) {
+ analyzerMode = 'static';
+}
+
+//
+// Common configuration chunk to be used for both
+// client-side (client.js) and server-side (server.js) bundles
+// -----------------------------------------------------------------------------
+
+const config = {
+ context: path.resolve(__dirname, '../src'),
+
+ output: {
+ path: path.resolve(__dirname, '../build/public/assets'),
+ publicPath: '/assets/',
+ pathinfo: isVerbose,
+ },
+
+ module: {
+ rules: [
+ {
+ test: /\.jsx?$/,
+ loader: 'babel-loader',
+ include: [
+ path.resolve(__dirname, '../src'),
+ ],
+ query: {
+ // https://github.com/babel/babel-loader#options
+ cacheDirectory: isDebug,
+
+ // https://babeljs.io/docs/usage/options/
+ babelrc: false,
+ presets: [
+ // A Babel preset that can automatically determine the Babel plugins and polyfills
+ // https://github.com/babel/babel-preset-env
+ ['env', {
+ targets: {
+ browsers: pkg.browserslist,
+ },
+ modules: false,
+ useBuiltIns: false,
+ debug: false,
+ }],
+ // Experimental ECMAScript proposals
+ // https://babeljs.io/docs/plugins/#presets-stage-x-experimental-presets-
+ 'stage-2',
+ // JSX, Flow
+ // https://github.com/babel/babel/tree/master/packages/babel-preset-react
+ 'react',
+ // Optimize React code for the production build
+ // https://github.com/thejameskyle/babel-react-optimize
+ ...isDebug ? [] : ['react-optimize'],
+ ],
+ plugins: [
+ // Adds component stack to warning messages
+ // https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-source
+ ...isDebug ? ['transform-react-jsx-source'] : [],
+ // Adds __self attribute to JSX which React will use for some warnings
+ // https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-self
+ ...isDebug ? ['transform-react-jsx-self'] : [],
+ ],
+ },
+ },
+ {
+ test: /\.css/,
+ use: [
+ {
+ loader: 'isomorphic-style-loader',
+ },
+ {
+ loader: 'css-loader',
+ options: {
+ // CSS Loader https://github.com/webpack/css-loader
+ importLoaders: 1,
+ sourceMap: isDebug,
+ // CSS Modules https://github.com/css-modules/css-modules
+ modules: true,
+ localIdentName: isDebug ? '[name]-[local]-[hash:base64:5]' : '[hash:base64:5]',
+ // CSS Nano http://cssnano.co/options/
+ minimize: !isDebug,
+ discardComments: { removeAll: true },
+ },
+ },
+ {
+ loader: 'postcss-loader',
+ options: {
+ config: './tools/postcss.config.js',
+ },
+ },
+ ],
+ },
+ {
+ test: /\.md$/,
+ loader: path.resolve(__dirname, './lib/markdown-loader.js'),
+ },
+ {
+ test: /\.txt$/,
+ loader: 'raw-loader',
+ },
+ {
+ test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/,
+ loader: 'file-loader',
+ query: {
+ name: isDebug ? '[path][name].[ext]?[hash:8]' : '[hash:8].[ext]',
+ },
+ },
+ {
+ test: /\.(mp4|webm|wav|mp3|m4a|aac|oga)(\?.*)?$/,
+ loader: 'url-loader',
+ query: {
+ name: isDebug ? '[path][name].[ext]?[hash:8]' : '[hash:8].[ext]',
+ limit: 10000,
+ },
+ },
+ ],
+ },
+
+ resolve: {
+ modules: [path.resolve(__dirname, '../src'), 'node_modules'],
+ },
+
+ // Don't attempt to continue if there are any errors.
+ bail: !isDebug,
+
+ cache: isDebug,
+
+ stats: {
+ colors: true,
+ reasons: isDebug,
+ hash: isVerbose,
+ version: isVerbose,
+ timings: true,
+ chunks: isVerbose,
+ chunkModules: isVerbose,
+ cached: isVerbose,
+ cachedAssets: isVerbose,
+ },
+};
+
+//
+// Configuration for the client-side bundle (client.js)
+// -----------------------------------------------------------------------------
+
+const clientConfig = {
+ ...config,
+
+ name: 'client',
+ target: 'web',
+
+ entry: {
+ client: ['babel-polyfill', './client.js'],
+ },
+
+ output: {
+ ...config.output,
+ filename: isDebug ? '[name].js' : '[name].[chunkhash:8].js',
+ chunkFilename: isDebug ? '[name].chunk.js' : '[name].[chunkhash:8].chunk.js',
+ },
+
+ resolve: { ...config.resolve },
+
+ plugins: [
+ // Define free variables
+ // https://webpack.github.io/docs/list-of-plugins.html#defineplugin
+ new webpack.DefinePlugin({
+ 'process.env.NODE_ENV': isDebug ? '"development"' : '"production"',
+ 'process.env.BROWSER': true,
+ __DEV__: isDebug,
+ }),
+
+ // Emit a file with assets paths
+ // https://github.com/sporto/assets-webpack-plugin#options
+ new AssetsPlugin({
+ path: path.resolve(__dirname, '../build'),
+ filename: 'assets.json',
+ prettyPrint: true,
+ }),
+
+ // Move modules that occur in multiple entry chunks to a new entry chunk (the commons chunk).
+ // http://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin
+ new webpack.optimize.CommonsChunkPlugin({
+ name: 'vendor',
+ minChunks: module => /node_modules/.test(module.resource),
+ }),
+
+ ...isDebug ? [] : [
+ // Minimize all JavaScript output of chunks
+ // https://github.com/mishoo/UglifyJS2#compressor-options
+ new webpack.optimize.UglifyJsPlugin({
+ sourceMap: true,
+ compress: {
+ screw_ie8: true, // React doesn't support IE8
+ warnings: isVerbose,
+ unused: true,
+ dead_code: true,
+ },
+ mangle: {
+ screw_ie8: true,
+ },
+ output: {
+ comments: false,
+ screw_ie8: true,
+ },
+ }),
+ ],
+
+ new BundleAnalyzerPlugin({
+ // See above
+ analyzerMode,
+ // Host that will be used in `server` mode to start HTTP server.
+ analyzerHost: '127.0.0.1',
+ // Port that will be used in `server` mode to start HTTP server.
+ analyzerPort,
+ // Path to bundle report file that will be generated in `static` mode.
+ // Relative to bundles output directory.
+ reportFilename: path.resolve(__dirname, '../report.html'),
+ // Automatically open report in default browser
+ openAnalyzer: true,
+ // If `true`, Webpack Stats JSON file will be generated in bundles output directory
+ generateStatsFile: !isDebug,
+ // Name of Webpack Stats JSON file that will be generated if `generateStatsFile` is `true`.
+ // Relative to bundles output directory.
+ statsFilename: path.resolve(__dirname, '../stats.json'),
+ // Options for `stats.toJson()` method.
+ // You can exclude sources of your modules from stats file with `source: false` option.
+ // See more options here: https://github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21
+ statsOptions: null,
+ // Log level. Can be 'info', 'warn', 'error' or 'silent'.
+ logLevel: 'info',
+ }),
+ ],
+
+ // Choose a developer tool to enhance debugging
+ // http://webpack.github.io/docs/configuration.html#devtool
+ devtool: isDebug ? 'cheap-module-source-map' : false,
+
+ // Some libraries import Node modules but don't use them in the browser.
+ // Tell Webpack to provide empty mocks for them so importing them works.
+ // https://webpack.github.io/docs/configuration.html#node
+ // https://github.com/webpack/node-libs-browser/tree/master/mock
+ node: {
+ fs: 'empty',
+ net: 'empty',
+ tls: 'empty',
+ },
+};
+
+//
+// Configuration for the server-side bundle (server.js)
+// -----------------------------------------------------------------------------
+
+const serverConfig = {
+ ...config,
+
+ name: 'server',
+ target: 'node',
+
+ entry: {
+ server: ['babel-polyfill', './server.js'],
+ },
+
+ output: {
+ ...config.output,
+ filename: '../../server.js',
+ libraryTarget: 'commonjs2',
+ },
+
+ module: {
+ ...config.module,
+
+ // Override babel-preset-env configuration for Node.js
+ rules: config.module.rules.map(rule => (rule.loader !== 'babel-loader' ? rule : {
+ ...rule,
+ query: {
+ ...rule.query,
+ presets: rule.query.presets.map(preset => (preset[0] !== 'env' ? preset : ['env', {
+ targets: {
+ node: parseFloat(pkg.engines.node.replace(/^\D+/g, '')),
+ },
+ modules: false,
+ useBuiltIns: false,
+ debug: false,
+ }])),
+ },
+ })),
+ },
+
+ resolve: { ...config.resolve },
+
+ externals: [
+ /^\.\/assets\.json$/,
+ (context, request, callback) => {
+ const isExternal =
+ request.match(/^[@a-z][a-z/.\-0-9]*$/i) &&
+ !request.match(/\.(css|less|scss|sss)$/i);
+ callback(null, Boolean(isExternal));
+ },
+ ],
+
+ plugins: [
+ // Define free variables
+ // https://webpack.github.io/docs/list-of-plugins.html#defineplugin
+ new webpack.DefinePlugin({
+ 'process.env.NODE_ENV': isDebug ? '"development"' : '"production"',
+ 'process.env.BROWSER': false,
+ __DEV__: isDebug,
+ }),
+
+ // Do not create separate chunks of the server bundle
+ // https://webpack.github.io/docs/list-of-plugins.html#limitchunkcountplugin
+ new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
+
+ // Adds a banner to the top of each generated chunk
+ // https://webpack.github.io/docs/list-of-plugins.html#bannerplugin
+ new webpack.BannerPlugin({
+ banner: 'require("source-map-support").install();',
+ raw: true,
+ entryOnly: false,
+ }),
+ ],
+
+ node: {
+ console: false,
+ global: false,
+ process: false,
+ Buffer: false,
+ __filename: false,
+ __dirname: false,
+ },
+
+ devtool: isDebug ? 'cheap-module-source-map' : 'source-map',
+};
+
+export default [clientConfig, serverConfig];