├── .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 |
--------------------------------------------------------------------------------