├── .babelrc ├── .flowconfig ├── postcss.config.js ├── src ├── components │ ├── Home.js │ ├── Home.test.js │ └── App.js ├── stores │ └── root_store.js ├── utils │ └── utils.js ├── styles │ └── App.scss ├── index.html └── index.js ├── Dockerfile ├── .eslintrc.js ├── .gitignore ├── package.json ├── webpack.rules.js └── webpack.config.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "stage-2", "react"] 3 | } -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "precss": {}, 4 | "postcss-font-magician": {}, 5 | "postcss-flexbugs-fixes": {}, 6 | "postcss-cssnext": {}, 7 | "css-declaration-sorter": {}, 8 | "css-mqpacker": {}, 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | 4 | const Home = ({ store }) =>
5 | Home component: 6 |

{store.testVal}

7 |
; 8 | 9 | export default inject("store")(observer(Home)); -------------------------------------------------------------------------------- /src/stores/root_store.js: -------------------------------------------------------------------------------- 1 | import { types } from "mobx-state-tree"; 2 | 3 | const RootStore = types.model("RootStore", { 4 | testVal: types.string 5 | }).actions(self => ({ 6 | updateTestVal(e) { 7 | self.testVal = e.target.value; 8 | } 9 | })); 10 | 11 | export default RootStore; -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | WORKDIR /code 3 | run apk --no-cache add --virtual native-deps \ 4 | g++ gcc libgcc libstdc++ linux-headers make python && \ 5 | npm install --quiet node-gyp -g && \ 6 | npm rebuild node-sass --force && \ 7 | apk del native-deps 8 | CMD ["yarn", "start"] 9 | EXPOSE 9000 -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | import { getSnapshot, applySnapshot } from "mobx-state-tree"; 2 | 3 | export const saveState = (module, store) => { 4 | if (module.hot.data && module.hot.data.store) { 5 | applySnapshot(store, module.hot.data.store); 6 | } 7 | module.hot.dispose(data => { 8 | data.store = getSnapshot(store); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module" 6 | }, 7 | "plugins": ["flowtype-errors"], 8 | "rules": { 9 | "flowtype-errors/show-errors": 2 10 | }, 11 | "env": { 12 | "browser": true, 13 | "node": true 14 | } 15 | }; -------------------------------------------------------------------------------- /src/components/Home.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import sinon from 'sinon'; 3 | //import { expect } from 'chai'; 4 | import { shallow } from 'enzyme'; 5 | 6 | import Home from "./Home.js"; 7 | 8 | describe("it should render ", () => { 9 | it("renders the app shell", () => { 10 | const wrapper = shallow(); 11 | expect(wrapper.find(".wrapper")).toExist; 12 | }) 13 | }); -------------------------------------------------------------------------------- /src/styles/App.scss: -------------------------------------------------------------------------------- 1 | @import "~normalize.css"; 2 | @import "~modularscale-sass"; 3 | @import "~sass-mediaqueries/media-queries"; 4 | 5 | .wrapper { 6 | display: flex; 7 | flex-flow: column; 8 | max-width: 480px; 9 | margin: 0 auto; 10 | text-align: center; 11 | font-family: "Cerebri Sans"; 12 | 13 | input { 14 | margin: 6px 0; 15 | } 16 | } 17 | 18 | .page { 19 | border: 1px solid #ddd; 20 | padding: 48px; 21 | margin: 24px 0; 22 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # dist folder 61 | dist -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require("viewport-units-buggyfill").init(); 2 | import React from "react"; 3 | import { render } from "react-dom"; 4 | import { BrowserRouter } from "react-router-dom"; 5 | import { Provider } from "mobx-react"; 6 | import { applySnapshot, getSnapshot } from "mobx-state-tree"; 7 | import { AppContainer } from "react-hot-loader"; 8 | 9 | import "styles/App"; 10 | import App from "components/App"; 11 | import RootStore from "stores/root_store"; 12 | 13 | const store = RootStore.create({ 14 | testVal: "Hello from Root Store" 15 | }) 16 | 17 | const renderApp = Component => { 18 | render( 19 | 20 | 21 | 22 | 23 | 24 | 25 | , 26 | document.getElementById("root") 27 | ); 28 | }; 29 | 30 | renderApp(App); 31 | 32 | if (module.hot) { 33 | module.hot.accept(["components/App", "stores/root_store"], () => { 34 | renderApp(App); 35 | }); 36 | 37 | if (module.hot.data && module.hot.data.store) { 38 | applySnapshot(store, module.hot.data.store); 39 | } 40 | module.hot.dispose(data => { 41 | data.store = getSnapshot(store); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { compose, mapProps } from "recompose"; 3 | import PropTypes from "prop-types"; 4 | import { types, getSnapshot, applySnapshot } from "mobx-state-tree"; 5 | import { inject, observer } from "mobx-react"; 6 | import { Route, Link, withRouter } from "react-router-dom"; 7 | import universal from "react-universal-component"; 8 | import Perimeter from "react-perimeter"; 9 | import DevTools from "mobx-react-devtools"; 10 | import { saveState } from "utils"; 11 | 12 | const Home = universal( 13 | props => import(/* webpackChunkName: "home" */ "components/Home"), 14 | { 15 | loading: () => null 16 | } 17 | ); 18 | 19 | const state = types.model({ 20 | testVal: types.string 21 | }).actions(self => ({ 22 | updateTestVal(e) { 23 | self.testVal = e.target.value 24 | } 25 | })).create({ 26 | testVal: "Hello from local component state" 27 | }); 28 | 29 | const App = ({ state, store }) => { 30 | return ( 31 |
32 | 33 | Home.preload()} padding={60}> 34 | Testroute 35 | 36 | state.updateTestVal(e)} 39 | value={state.testVal} 40 | /> 41 | 46 | 47 | Local component state with MST: 48 |

{state.testVal}

49 | Global store with MST: 50 |

{store.testVal}

51 |
52 | ); 53 | }; 54 | 55 | export default compose(mapProps(() => ({ state: state })))( 56 | withRouter(inject("store")(observer(App))) 57 | ); 58 | 59 | if (module.hot) { 60 | saveState(module, state); 61 | } 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "npm run clean && webpack-dev-server --config webpack.config.babel.js", 4 | "test": "NODE_ENV=test jest --forceExit", 5 | "build": "npm run clean && NODE_ENV=production webpack --config webpack.config.babel.js -p && js-beautify ./dist/index.*.html -r", 6 | "clean": "rm -rf ./dist && rm -rf ./test" 7 | }, 8 | "jest": { 9 | "transform": { 10 | "^.+\\js?$": "babel-jest" 11 | }, 12 | "collectCoverageFrom": [ 13 | "./src/*.test.js" 14 | ] 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.26.0", 18 | "babel-eslint": "^8.0.0", 19 | "babel-jest": "^21.0.2", 20 | "babel-loader": "^7.1.2", 21 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 22 | "babel-polyfill": "^6.26.0", 23 | "babel-preset-env": "^1.6.0", 24 | "babel-preset-flow": "^6.23.0", 25 | "babel-preset-react": "^6.24.1", 26 | "babel-preset-stage-2": "^6.24.1", 27 | "chai": "^4.1.2", 28 | "cross-env": "^5.0.5", 29 | "css-declaration-sorter": "^2.1.0", 30 | "css-loader": "^0.28.5", 31 | "css-mqpacker": "^6.0.1", 32 | "enzyme": "^2.9.1", 33 | "eslint": "^4.7.1", 34 | "eslint-loader": "^1.9.0", 35 | "eslint-plugin-flowtype-errors": "^3.3.1", 36 | "extract-text-webpack-plugin": "^3.0.0", 37 | "file-loader": "^0.11.2", 38 | "flow-bin": "^0.54.1", 39 | "html-webpack-plugin": "^2.30.1", 40 | "image-webpack-loader": "^3.3.1", 41 | "jest": "^21.0.2", 42 | "js-beautify": "^1.6.14", 43 | "jsdom": "^11.2.0", 44 | "modularscale-sass": "^3.0.3", 45 | "node-sass": "^4.5.3", 46 | "normalize.css": "^7.0.0", 47 | "postcss-cssnext": "^3.0.2", 48 | "postcss-flexbugs-fixes": "^3.2.0", 49 | "postcss-font-magician": "^2.0.0", 50 | "postcss-loader": "^2.0.6", 51 | "precss": "^2.0.0", 52 | "preload-webpack-plugin": "^2.0.0", 53 | "prettier": "^1.7.0", 54 | "react-addons-test-utils": "^15.6.0", 55 | "react-hot-loader": "next", 56 | "react-test-renderer": "^15.6.1", 57 | "resolve-url-loader": "^2.1.0", 58 | "sass-loader": "^6.0.6", 59 | "sass-mediaqueries": "^1.6.1", 60 | "sinon": "^3.2.1", 61 | "style-loader": "^0.18.2", 62 | "viewport-units-buggyfill": "^0.6.2", 63 | "webpack": "^3.5.6", 64 | "webpack-dev-server": "^2.7.1", 65 | "webpack-node-externals": "^1.6.0" 66 | }, 67 | "dependencies": { 68 | "mobx": "latest", 69 | "mobx-react": "latest", 70 | "mobx-react-devtools": "^4.2.15", 71 | "mobx-state-tree": "latest", 72 | "prop-types": "^15.5.10", 73 | "react": "^15.6.1", 74 | "react-dom": "^15.6.1", 75 | "react-perimeter": "^0.3.1", 76 | "react-router-dom": "^4.2.2", 77 | "react-universal-component": "^2.5.1", 78 | "recompose": "^0.25.0", 79 | "styled-components": "^2.1.2" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /webpack.rules.js: -------------------------------------------------------------------------------- 1 | import ExtractTextPlugin from "extract-text-webpack-plugin"; 2 | import { resolve } from "path"; 3 | 4 | // Define env 5 | const isProduction = process.env.NODE_ENV === "production"; 6 | 7 | export const eslint = () => { 8 | return { 9 | enforce: "pre", 10 | test: /\.js$/, 11 | exclude: /node_modules/, 12 | loader: "eslint-loader" 13 | }; 14 | }; 15 | 16 | // JS loader 17 | export const js = () => { 18 | if (isProduction) { 19 | return { 20 | test: /\.js$/, 21 | exclude: /(node_modules|bower_components)/, 22 | use: { 23 | loader: "babel-loader", 24 | options: { 25 | babelrc: false, 26 | presets: [["env", { modules: false }], "react", "stage-2"] 27 | } 28 | } 29 | }; 30 | } else { 31 | return { 32 | test: /\.js$/, 33 | exclude: /(node_modules|bower_components)/, 34 | use: { 35 | loader: "babel-loader", 36 | options: { 37 | babelrc: false, 38 | presets: [["env", { modules: false }], "react", "stage-2"], 39 | plugins: ["react-hot-loader/babel"] 40 | } 41 | } 42 | }; 43 | } 44 | }; 45 | 46 | // Style loader 47 | export const styles = () => { 48 | if (isProduction) { 49 | return { 50 | test: /\.css|scss$/, 51 | use: ExtractTextPlugin.extract({ 52 | fallback: "style-loader", 53 | use: [ 54 | "css-loader", 55 | { 56 | loader: "postcss-loader", 57 | options: { 58 | sourceMap: true 59 | } 60 | }, 61 | "resolve-url-loader", 62 | { 63 | loader: "sass-loader", 64 | options: { 65 | sourceMap: true 66 | } 67 | } 68 | ] 69 | }) 70 | }; 71 | } else { 72 | return { 73 | test: /\.css|scss$/, 74 | use: [ 75 | "style-loader", 76 | "css-loader", 77 | { 78 | loader: "postcss-loader", 79 | options: { 80 | sourceMap: true 81 | } 82 | }, 83 | "resolve-url-loader", 84 | { 85 | loader: "sass-loader", 86 | options: { 87 | sourceMap: true 88 | } 89 | } 90 | ] 91 | }; 92 | } 93 | }; 94 | 95 | // Image loader 96 | export const images = () => { 97 | return { 98 | test: /\.(jpe?g|png|gif|svg)$/i, 99 | exclude: resolve(__dirname, "src", "webfonts"), 100 | use: [ 101 | { 102 | loader: "image-webpack-loader", 103 | options: { 104 | mozjpeg: { 105 | progressive: true 106 | }, 107 | gifsicle: { 108 | interlaced: false 109 | }, 110 | optipng: { 111 | optimizationLevel: 4 112 | }, 113 | pngquant: { 114 | quality: "75-90", 115 | speed: 4 116 | } 117 | } 118 | } 119 | ] 120 | }; 121 | }; 122 | 123 | // Webfont loader 124 | export const webfonts = () => { 125 | return { 126 | test: /\.(eot|svg|ttf|woff|woff2)$/, 127 | use: ["file-loader?name=[name].[hash].[ext]"] 128 | }; 129 | }; 130 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from "webpack"; 2 | import { resolve } from "path"; 3 | import ExtractTextPlugin from "extract-text-webpack-plugin"; 4 | import HtmlWebpackPlugin from "html-webpack-plugin"; 5 | import PreloadWebpackPlugin from "preload-webpack-plugin"; 6 | import { eslint, js, styles, images, webfonts } from "./webpack.rules"; 7 | 8 | // Get env 9 | const isProduction = process.env.NODE_ENV === "production"; 10 | 11 | // Define chunks to prefetch 12 | const prefetchChunks = ["home"]; 13 | 14 | // Dev server config 15 | const devServer = { 16 | hot: true, 17 | contentBase: resolve(__dirname, "dist"), 18 | port: 9000, 19 | host: '0.0.0.0', 20 | historyApiFallback: true, 21 | publicPath: "/", 22 | headers: { 'Access-Control-Allow-Origin': '*' } 23 | }; 24 | 25 | // Base config 26 | const getBase = () => { 27 | let base = {}; 28 | base.devtool = isProduction ? "source-map" : "eval"; 29 | base.resolve = { 30 | alias: { 31 | components: resolve(__dirname, "src/components"), 32 | stores: resolve(__dirname, "src/stores"), 33 | utils: resolve(__dirname, "src/utils/utils.js"), 34 | styles: resolve(__dirname, "src/styles") 35 | }, 36 | extensions: [".js", ".jsx", ".scss", ".css", "*"] 37 | }; 38 | if (!isProduction) { 39 | base.devServer = devServer; 40 | } 41 | return base; 42 | }; 43 | 44 | // Entry 45 | const getEntry = () => { 46 | let entry; 47 | if (isProduction) { 48 | entry = { 49 | main: ["./src/index"], 50 | vendor: ["react", "react-dom"] 51 | }; 52 | } else { 53 | entry = [ 54 | "react-hot-loader/patch", 55 | "webpack-dev-server/client?http://localhost:9000", 56 | "webpack/hot/only-dev-server", 57 | "./src/index" 58 | ]; 59 | } 60 | 61 | return entry; 62 | }; 63 | 64 | // Output 65 | const getOutput = () => { 66 | let output = { 67 | filename: isProduction ? "assets/js/[name].[chunkhash].app.js" : "assets/js/[name].app.js", 68 | chunkFilename: isProduction ? "assets/js/[name].[chunkhash].app.js" : "assets/js/[name].app.js", 69 | path: resolve(__dirname, "dist"), 70 | publicPath: "/" 71 | }; 72 | 73 | return output; 74 | }; 75 | 76 | // Rules & Loaders 77 | const getRules = () => { 78 | const rules = []; 79 | rules.push(eslint()); 80 | rules.push(js()); 81 | rules.push(styles()); 82 | rules.push(images()); 83 | rules.push(webfonts()); 84 | 85 | return rules; 86 | }; 87 | 88 | // Plugins 89 | const getPlugins = () => { 90 | const plugins = []; 91 | if (isProduction) { 92 | plugins.push(new webpack.HashedModuleIdsPlugin()); 93 | plugins.push( 94 | new webpack.optimize.CommonsChunkPlugin({ 95 | name: ["vendor"], 96 | filename: "assets/js/[name].[hash].js" 97 | }) 98 | ); 99 | plugins.push( 100 | new webpack.optimize.UglifyJsPlugin({ 101 | sourceMap: true, 102 | minimize: true, 103 | compress: { warnings: false, drop_console: true, screw_ie8: true }, 104 | output: { comments: false } 105 | }) 106 | ); 107 | plugins.push( 108 | new ExtractTextPlugin({ 109 | filename: "assets/css/styles.[contenthash].css", 110 | allChunks: true 111 | }) 112 | ); 113 | plugins.push( 114 | new HtmlWebpackPlugin({ 115 | inject: true, 116 | filename: "index.[chunkhash].html", 117 | template: resolve(__dirname, "src", "index.html"), 118 | chunksSortMode: "dependency" 119 | }) 120 | ); 121 | plugins.push(new PreloadWebpackPlugin({ 122 | rel: "prefetch", 123 | include: [...prefetchChunks, "vendor", "main"] 124 | })); 125 | } else { 126 | plugins.push(new webpack.NamedModulesPlugin()); 127 | plugins.push(new webpack.HotModuleReplacementPlugin()); 128 | plugins.push( 129 | new HtmlWebpackPlugin({ 130 | inject: true, 131 | template: resolve(__dirname, "src", "index.html"), 132 | chunksSortMode: "dependency" 133 | }) 134 | ); 135 | } 136 | 137 | return plugins; 138 | }; 139 | 140 | // All together now... 141 | export default { 142 | ...getBase(), 143 | entry: getEntry(), 144 | module: { 145 | rules: getRules() 146 | }, 147 | plugins: getPlugins(), 148 | output: getOutput() 149 | }; 150 | --------------------------------------------------------------------------------