diff --git a/.babelrc b/.babelrc index ba1a88e..306fd49 100755 --- a/.babelrc +++ b/.babelrc @@ -1,19 +1,22 @@ { "plugins": [ + "transform-decorators-legacy", "transform-class-properties", - "syntax-decorators", - "transform-decorators-legacy" + "syntax-decorators" ], "presets": [ ["es2015", { "modules": false }], - "react", - "stage-0" + "stage-0", + "react" ], "env": { "development": { - "presets": [ - "react-hmre" + "plugins": [ + "react-hot-loader/babel" ] + }, + "production": { + "presets": ["react-optimize"] } } } diff --git a/.eslintrc b/.eslintrc index 690d22c..dd1ffc5 100755 --- a/.eslintrc +++ b/.eslintrc @@ -23,13 +23,6 @@ rules: arrow-body-style: 0 arrow-parens: 0 class-methods-use-this: 0 - func-names: 0 - indent: 2 - new-cap: 0 - no-plusplus: 0 - no-return-assign: 0 - quote-props: 0 - template-curly-spacing: [2, "always"] comma-dangle: ["error", { "arrays": "always-multiline", "objects": "always-multiline", @@ -37,13 +30,23 @@ rules: "exports": "always-multiline", "functions": "never" }] + func-names: 0 + indent: 2 jsx-quotes: [2, "prefer-single"] - react/forbid-prop-types: 0 - react/jsx-curly-spacing: [2, "always"] - react/jsx-filename-extension: 0 - react/jsx-boolean-value: 0 - react/prefer-stateless-function: 0 + new-cap: 0 + no-plusplus: 0 + no-return-assign: 0 + quote-props: 0 + template-curly-spacing: [2, "always"] + # import rules import/extensions: 0 import/no-extraneous-dependencies: 0 import/no-unresolved: 0 import/prefer-default-export: 0 + # react rules + react/forbid-prop-types: 0 + react/jsx-boolean-value: 0 + react/jsx-curly-spacing: [2, "always"] + react/jsx-filename-extension: 0 + react/prefer-stateless-function: 0 + react/require-default-props: 0 diff --git a/.gitignore b/.gitignore index 102ce0f..54a54a9 100755 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build .module-cache *.log* _nogit +todo.md diff --git a/README.md b/README.md index c4fb8be..2935d87 100755 --- a/README.md +++ b/README.md @@ -14,14 +14,18 @@ Name comes from a fictional character [Marvin](https://en.wikipedia.org/wiki/Mar * [What is this?](#user-content-what-is-this) * [Features](#user-content-features) * [Setup](#user-content-setup) +* [npm tasks](#user-content-npm-tasks) * [Running in dev mode](#user-content-running-in-dev-mode) -* [Build (production)](#user-content-build-production) -* [Running in preview production mode](#user-content-running-in-preview-production-mode) +* [Running it with webpack dashboard](#user-content-running-it-with-webpack-dashboard) +* [Build client (production)](#user-content-build-client-production) +* [Running client in preview production mode](#user-content-running-client-in-preview-production-mode) +* [Universal dev mode](#user-content-universal-dev-mode) +* [Universal build (production)](#user-content-universal-build-production) +* [Removing server rendering related stuff](#user-content-removing-server-rendering-related-stuff) * [Linting](#user-content-linting) * [Git hooks](#user-content-git-hooks) * [Changelog](#user-content-changelog) - ## What is this? Boilerplate for kicking off React/Redux applications. @@ -49,9 +53,10 @@ By complete we mean it has examples for: - [x] Redux - [x] Redux Thunk - [x] Redux DevTools (you need to have [browser extension](https://github.com/zalmoxisus/redux-devtools-extension) installed) -- [x] Immutable reducer data -- [x] Webpack 2 (development and production config) +- [x] Universal rendering +- [x] Webpack 3 (development and production config) - [x] Hot Module Replacement +- [x] Immutable reducer data - [x] Babel - static props, decorators - [x] SASS with autoprefixing - [x] Webpack dashboard @@ -60,30 +65,38 @@ By complete we mean it has examples for: - [x] Preview production build - [x] File imports relative to the app root - [x] Git hooks - lint before push +- [x] Tree shaking build +- [x] Import SVGs as React components ## TODO -- [x] Tree shaking build - [ ] Switch to [redux-saga](https://github.com/redux-saga/redux-saga) -- [ ] Universal rendering - [ ] Server async data - [ ] Internationalization -Other nice to have features - -- [x] Generating ~~icon font from SVG~~ SVG sprite -- [ ] Feature detection (Modernizr) (?) -- [ ] Google analytics (?) -- [ ] Error reporting (?) ## Setup -Tested with node 6.x and 7.x +Tested with node 7.x and 8.x ``` $ npm install ``` +## npm tasks + +* `start` - starts client app only in development mode, using webpack dev server +* `client:dev` - same as `start` plus fancy webpack dashboard +* `client:watch` - not to be used on it's own, starts webpack with client config in watch mode +* `client:build` - builds client application +* `client:preview` - runs client application in *production* mode, using webpack dev server (use for local testing of the client production build) +* `server:watch` - not to be used on it's own, starts webpack with server config in watch mode +* `server:restart` - not to be used on it's own, server build run using `nodemon` +* `server:build` - not to be used on it's own, builds server application +* `server:dev` - starts server app only in development mode (use for testing server responses) +* `universal:dev` - runs both server and client in watch mode, automatically restarts server on changes +* `universal:build` - builds both server and client + ## Running in dev mode ``` @@ -105,7 +118,7 @@ $ npm run dev **OS X Terminal.app users:** Make sure that **View ā†’ Allow Mouse Reporting** is enabled, otherwise scrolling through logs and modules won't work. If your version of Terminal.app doesn't have this feature, you may want to check out an alternative such as [iTerm2](https://www.iterm2.com/). -## Build (production) +## Build client (production) Build will be placed in the `build` folder. @@ -133,7 +146,7 @@ const publicPath = '/your-app/'; Don't forget the trailing slash (`/`). In development visit `http://localhost:3000/your-app/`. -## Running in preview production mode +## Running client in preview production mode This command will start webpack dev server, but with `NODE_ENV` set to `production`. Everything will be minified and served. @@ -143,60 +156,86 @@ Hot reload will not work, so you need to refresh the page manually after changin npm run preview ``` -## Linting - -For linting I'm using [eslint-config-airbnb](https://www.npmjs.com/package/eslint-config-airbnb), -but some options are overridden to my personal preferences. +## Universal dev mode ``` -$ npm run lint +npm run universal:dev ``` -## Git hooks +Visit `http://localhost:8080/` from your browser of choice. +Server is visible from the local network as well. -Linting pre-push hook is not enabled by default. -It will prevent the push if lint task fails, -but you need to add it manually by running: +## Universal build (production) ``` -npm run hook-add +npm run universal:build ``` -To remove it, run this task: +copy `package.json` and `build` folder to your production server + +install only production dependencies and run server ``` -npm run hook-remove +npm install --production + +node ./build/server.js ``` -## Components +## Removing server rendering related stuff -### SVG icons - `Icon` +If you are not using server rendering remove following packages from `package.json` -Add SVG icons to `source/assets/icons` folder, and they will automatically be added to SVG sprite. +* `express` +* `transit-immutable-js` +* `transit-js` +* `nodemon` +* `concurrently` -**Usage:** +Also open `source/js/config/store.js` and remove lines marked with the following comment ``` -import Icon from 'components/Global/Icon'; +// Remove if you are not using server rendering +``` + +Client app is going to work without this but, it will include few unused packages. +Therefore it is better to remove them. + + +## Linting + +For linting I'm using [eslint-config-airbnb](https://www.npmjs.com/package/eslint-config-airbnb), +but some options are overridden to my personal preferences. - +``` +$ npm run lint ``` -**Available props** +## Git hooks + +Linting pre-push hook is not enabled by default. +It will prevent the push if lint task fails, +but you need to add it manually by running: ``` -glyph // required, name of the SVG icon -className // optional, additional CSS class, default ones are `Icon Icon--iconName` -width // optional, default 24 -height // optional, default 24 -style // optional, CSS style object +npm run hook-add ``` +To remove it, run this task: + +``` +npm run hook-remove +``` ----- ## Changelog +#### 0.2.0 + +* Webpack updated to v3 and rewritten webpack config +* Optional universal rendering +* A lot of code changes + #### 0.1.7 * Migrated to React Router 4.x (thanks @shams-ali) diff --git a/package.json b/package.json index ddf8300..d759fd7 100755 --- a/package.json +++ b/package.json @@ -1,75 +1,89 @@ { - "name": "react-redux-webpack2-boilerplate", - "version": "0.1.7", + "name": "marvin", + "version": "0.2.0", "private": false, "license": "MIT", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", "start": "webpack-dev-server", - "dev": "webpack-dashboard -t 'Marvin' -- webpack-dev-server", - "build": "rm -rf ./build && NODE_ENV=\"production\" webpack", - "lint-break-on-errors": "eslint ./source/js ./webpack.config.js -f table --ext .js --ext .jsx", - "lint": "eslint ./source/js ./webpack.config.js -f table --ext .js --ext .jsx || true", - "preview": "rm -rf ./build && NODE_ENV=\"production\" webpack-dashboard -t 'Preview Mode - Marvin' -- webpack-dev-server", + "client:dev": "webpack-dashboard -t 'Marvin' -- webpack-dev-server", + "client:watch": "webpack", + "client:build": "npm run clean && NODE_ENV=\"production\" webpack", + "client:preview": "npm run clean && NODE_ENV=\"production\" webpack-dev-server", + "server:watch": "SERVER_RENDER=\"true\" webpack --config webpack.config.server.js", + "server:restart": "nodemon ./build/server.js", + "server:build": "SERVER_RENDER=\"true\" NODE_ENV=\"production\" webpack --config webpack.config.server.js", + "server:dev": "npm run clean && concurrently --prefix \"[{name}]\" --names \"webpack-server,watch-server\" --kill-others \"npm run server:watch\" \"npm run server:restart\"", + "universal:dev": "concurrently --prefix \"[{name}]\" --names \"webpack-server,webpack-client,watch-server\" --kill-others \"npm run server:watch\" \"npm run client:watch\" \"npm run server:restart\"", + "universal:build": "npm run client:build && npm run server:build", + "clean": "rm -rf ./build", + "lint-hook": "eslint ./source/js ./webpack ./*.js -f table --ext .js --ext .jsx", + "lint": "npm run lint-hook --silent || true", "hook-add": "prepush install", - "hook-remove": "prepush remove" + "hook-remove": "prepush remove", + "test": "echo \"Error: no test specified\" && exit 1" }, "devDependencies": { - "autoprefixer": "^6.5.3", + "autoprefixer": "^7.1.2", "babel-core": "^6.7.2", - "babel-eslint": "^7.1.0", - "babel-loader": "^6.2.4", + "babel-eslint": "^7.2.3", + "babel-loader": "^7.1.1", "babel-plugin-syntax-decorators": "^6.13.0", "babel-plugin-transform-class-properties": "^6.6.0", "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-runtime": "^6.6.0", "babel-preset-es2015": "^6.22.0", "babel-preset-react": "^6.5.0", - "babel-preset-react-hmre": "^1.1.1", + "babel-preset-react-optimize": "^1.0.1", "babel-preset-stage-0": "^6.16.0", "babel-runtime": "^6.6.1", - "css-loader": "0.14.5", - "eslint": "^3.10.1", - "eslint-config-airbnb": "^13.0.0", - "eslint-plugin-import": "^2.2.0", - "eslint-plugin-jsx-a11y": "^2.2.3", - "eslint-plugin-react": "^6.7.1", - "extract-text-webpack-plugin": "^2.0.0", - "file-loader": "^0.9.0", + "concurrently": "^3.4.0", + "css-loader": "^0.28.4", + "eslint": "^4.3.0", + "eslint-config-airbnb": "^15.1.0", + "eslint-plugin-import": "^2.7.0", + "eslint-plugin-jsx-a11y": "^5.1.1", + "eslint-plugin-react": "^7.1.0", + "extract-text-webpack-plugin": "^3.0.0", "html-webpack-plugin": "^2.24.1", - "node-sass": "^3.13.0", - "postcss-loader": "^1.1.1", + "node-sass": "^4.5.3", + "nodemon": "^1.11.0", + "postcss-loader": "^2.0.6", "prepush": "^3.1.11", - "redux-logger": "^2.7.4", - "sass-loader": "^4.0.2", - "style-loader": "^0.13.0", - "svg-sprite-loader": "^2.1.0", + "react-hot-loader": "^3.0.0-beta.7", + "react-svg-loader": "^1.1.1", + "redux-logger": "^3.0.6", + "sass-loader": "^6.0.6", + "style-loader": "^0.18.2", "svgo": "^0.7.2", "svgo-loader": "^1.2.1", - "url-loader": "^0.5.7", - "webpack": "^2.2.1", + "webpack": "^3.4.1", "webpack-dashboard": "^0.4.0", - "webpack-dev-server": "^2.2.1" + "webpack-dev-server": "^2.6.1" }, "dependencies": { "babel-polyfill": "^6.23.0", - "es6-promise": "^3.3.1", + "es6-promise": "^4.1.1", + "eslint": "^3.19.0", + "express": "^4.15.3", + "file-loader": "^0.11.2", "immutable": "^3.8.1", "isomorphic-fetch": "^2.2.1", "prop-types": "^15.5.10", "react": "^15.5.4", "react-dom": "^15.5.4", - "react-redux": "^4.4.8", + "react-redux": "^5.0.5", "react-router": "^4.1.1", "react-router-dom": "^4.1.1", "redux": "^3.6.0", - "redux-thunk": "^2.2.0" + "redux-thunk": "^2.2.0", + "transit-immutable-js": "^0.7.0", + "transit-js": "^0.8.846" }, - "description": "Starter boilerplate for React and Redux, using Webpack 2", + "description": "Starter boilerplate for React and Redux, using Webpack 3", "repository": { "type": "git", - "url": "git+ssh://git@github.com/Stanko/react-redux-webpack2-boilerplate.git" + "url": "git+ssh://git@github.com/workco/marvin.git" }, "keywords": [ "react", @@ -79,10 +93,10 @@ ], "author": "Stanko", "bugs": { - "url": "https://github.com/Stanko/react-redux-webpack2-boilerplate/issues" + "url": "https://github.com/workco/marvin/issues" }, - "homepage": "https://github.com/Stanko/react-redux-webpack2-boilerplate#readme", + "homepage": "https://github.com/workco/marvin#readme", "prepush": [ - "npm run lint-break-on-errors --silent" + "npm run lint-hook --silent" ] } diff --git a/source/assets/img/workco-logo.svg b/source/assets/img/workco-logo.svg index e2488b9..6d9ae43 100644 --- a/source/assets/img/workco-logo.svg +++ b/source/assets/img/workco-logo.svg @@ -1,7 +1,7 @@ - + @@ -18,4 +18,4 @@ - \ No newline at end of file + diff --git a/source/assets/icons/circle.svg b/source/assets/svg/circle.svg similarity index 100% rename from source/assets/icons/circle.svg rename to source/assets/svg/circle.svg diff --git a/source/assets/icons/square.svg b/source/assets/svg/square.svg similarity index 100% rename from source/assets/icons/square.svg rename to source/assets/svg/square.svg diff --git a/source/assets/icons/triangle.svg b/source/assets/svg/triangle.svg similarity index 100% rename from source/assets/icons/triangle.svg rename to source/assets/svg/triangle.svg diff --git a/source/js/api/index.js b/source/js/api/index.js index 048864e..ae00a00 100644 --- a/source/js/api/index.js +++ b/source/js/api/index.js @@ -1,18 +1,11 @@ -import 'es6-promise'; +import promisePolyfill from 'es6-promise'; +import 'isomorphic-fetch'; -function testAsync() { - return new Promise(resolve => { - setTimeout(() => { - const date = new Date(); - let seconds = date.getSeconds(); - let minutes = date.getMinutes(); - - seconds = seconds < 10 ? `0${ seconds }` : seconds; - minutes = minutes < 10 ? `0${ minutes }` : minutes; +promisePolyfill.polyfill(); - resolve(`Current time: ${ date.getHours() }:${ minutes }:${ seconds }`); - }, (Math.random() * 1000) + 1000); // 1-2 seconds delay - }); +function testAsync() { + return fetch('http://date.jsontest.com/') + .then(response => response.json()); } export default { diff --git a/source/js/client.js b/source/js/client.js new file mode 100644 index 0000000..7f3ef49 --- /dev/null +++ b/source/js/client.js @@ -0,0 +1,40 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { AppContainer } from 'react-hot-loader'; +import { Provider } from 'react-redux'; +import 'babel-polyfill'; + +import configureStore from 'config/store'; +import Client from 'views/Client'; + +import es6Promise from 'es6-promise'; +import 'isomorphic-fetch'; + +// Load SCSS +import '../scss/app.scss'; + +es6Promise.polyfill(); + +const store = configureStore(); + +const render = Component => { + ReactDOM.render( + + + + + , + document.getElementById('root') + ); +}; + +// Render app +render(Client); + +if (module.hot) { + module.hot.accept('./views/Client/', () => { + const NewClient = require('./views/Client/index').default; // eslint-disable-line global-require + + render(NewClient); + }); +} diff --git a/source/js/components/Global/Icon.jsx b/source/js/components/Global/Icon.jsx deleted file mode 100644 index 888b8b8..0000000 --- a/source/js/components/Global/Icon.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -// Load all icons -const svgIcons = require.context('../../../assets/icons', false, /.*\.svg$/); - -function requireAll(requireContext) { - return requireContext.keys().map(requireContext); -} - -const icons = requireAll(svgIcons).reduce( - (state, icon) => ({ - ...state, - [icon.default.split('#')[1].replace('-usage', '')]: icon.default, - }), - {} -); - -export default class Icon extends Component { - render() { - const { - className, - width, - height, - glyph, - style, - } = this.props; - - const combinedClassName = className ? `Icon Icon--${ glyph } ${ className }` : `Icon Icon--${ glyph }`; - - return ( - - - - ); - } -} - -Icon.propTypes = { - width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - glyph: PropTypes.string.isRequired, - className: PropTypes.string, - style: PropTypes.object, -}; - -Icon.defaultProps = { - height: 24, - width: 24, -}; diff --git a/source/js/components/Global/Menu.jsx b/source/js/components/Global/Menu.jsx index 4d1747d..e365396 100644 --- a/source/js/components/Global/Menu.jsx +++ b/source/js/components/Global/Menu.jsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import { NavLink } from 'react-router-dom'; -import { routeCodes } from '../../views/App'; +import { routeCodes } from 'config/routes'; import workAndCoLogoImg from '../../../assets/img/workco-logo.svg'; export default class Menu extends Component { @@ -32,7 +32,7 @@ export default class Menu extends Component { 404 diff --git a/source/js/components/Global/RouteStatus.jsx b/source/js/components/Global/RouteStatus.jsx new file mode 100644 index 0000000..87bda57 --- /dev/null +++ b/source/js/components/Global/RouteStatus.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Route } from 'react-router-dom'; + +// This component is used for Server rendering +// When you want to return 40x http statuses + +const RouteStatus = ({ code, children }) => ( + { + if (staticContext) { + staticContext.status = code; // eslint-disable-line no-param-reassign + } + + return children; + } + } + /> +); + +RouteStatus.propTypes = { + code: PropTypes.number.isRequired, + children: PropTypes.object, +}; + +export default RouteStatus; diff --git a/source/js/config/routes.jsx b/source/js/config/routes.jsx new file mode 100644 index 0000000..cd5bbe5 --- /dev/null +++ b/source/js/config/routes.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import Dashboard from 'views/Dashboard'; +import About from 'views/About'; +import NotFound from 'views/NotFound'; + +const publicPath = '/'; + +export const routeCodes = { + DASHBOARD: publicPath, + ABOUT: `${ publicPath }about`, +}; + +export default () => ( + + + + + +); diff --git a/source/js/config/server-html.jsx b/source/js/config/server-html.jsx new file mode 100644 index 0000000..5620cf9 --- /dev/null +++ b/source/js/config/server-html.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import ReactDOMServer from 'react-dom/server'; +import PropTypes from 'prop-types'; +import { outputFiles } from '../../../webpack/output-files'; + +const ServerHtml = ({ appHtml, dehydratedState }) => ( + + + + + + Marvin • React/Redux Boilerplate + + + + +
+