├── .circleci └── config.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── scripts ├── build.sh ├── clean.sh ├── config │ ├── webpack.dev.js │ ├── webpack.prd.js │ └── webpack.shared.js ├── deploy.sh ├── dev.sh ├── devClient.sh ├── devServer.sh ├── lint.sh ├── precommit.sh ├── prod.sh ├── test.sh ├── testE2E.sh ├── testFunc.sh └── testUnit.sh └── src ├── client ├── components │ ├── App │ │ └── index.jsx │ ├── Dashboard │ │ ├── __tests__ │ │ │ └── e2e.js │ │ ├── index.jsx │ │ └── styles.css │ ├── Form │ │ ├── index.jsx │ │ └── styles.css │ ├── Login │ │ ├── __tests__ │ │ │ └── e2e.js │ │ ├── index.jsx │ │ └── styles.css │ └── Router │ │ └── index.jsx ├── containers │ ├── Dashboard.jsx │ └── Login.jsx ├── contexts │ ├── auth.jsx │ └── router.jsx ├── favicon.png ├── index.css ├── index.html └── index.jsx └── server ├── api.js ├── constants └── env.js ├── controllers └── users │ ├── __tests__ │ └── func.js │ └── index.js ├── index.js └── utils ├── errors ├── __tests__ │ └── unit.js └── index.js └── hot └── index.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:latest-browsers 6 | steps: 7 | - checkout 8 | - run: npm install 9 | - run: npm run lint 10 | - run: npm run test:unit 11 | - run: npm run test:func 12 | - run: npm run test:e2e 13 | - run: npm run deploy -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Directories 2 | coverage 3 | dist 4 | node_modules 5 | 6 | # Files 7 | *.log 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry = https://registry.npmjs.org/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Justin Sisley 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | mostly 3 |

4 | 5 |

6 | mostly 7 |

8 | 9 |

10 | They mostly come at night; mostly. 11 |
12 | Express + React + Babel + Webpack + Prettier + ESLint + Jest + Puppeteer 13 |

14 | 15 |

16 | GitHub release CircleCI license 17 |

18 | 19 | --- 20 | 21 | __mostly__ is a full-stack web application starter kit built on [Node.js](https://nodejs.org/). It uses [Express](https://expressjs.com/) for the server and [React](https://reactjs.org/) for the user interface. 22 | 23 | Its purpose is to serve as a lightweight, easy-to-comprehend starting point, with a focus on providing a great developer experience while helping you get high quality and maintainable apps deployed rapidly. 24 | 25 | __Nothing is hidden, nothing is magical__, and all of the "plumbing" is accessible and relatively simple. 26 | 27 | --- 28 | 29 | # Table of Contents 30 | 31 | - [Features](#features) 32 | - [Documentation](#documentation) 33 | - [Install](#install) 34 | - [Configuration](#configuration) 35 | - [Develop](#develop) 36 | - [Test](#test) 37 | - [Unit Tests](#unit-tests) 38 | - [Functional Tests](#functional-tests-api-endpoint-tests) 39 | - [End-to-end Tests](#end-to-end-tests-user-interface-tests) 40 | - [Test Modes and Options](#test-modes-and-options) 41 | - [Build](#build) 42 | - [Production](#production) 43 | - [Deployment](#deployment) 44 | - [Pre-commit Hook](#pre-commit-hook) 45 | - [Continuous Delivery](#continuous-delivery) 46 | - [(in)Frequently Asked Questions](#faq) 47 | - [Releases](https://github.com/justinsisley/mostly/releases) 48 | - [Credits](#credits) 49 | 50 | # Features 51 | 52 | - __Uses a minimal set of UI development tools__ _(via React and CSS modules)_ 53 | - __Uses a familiar Node.js HTTP server library__ _(via Express)_ 54 | - __Lets you use the latest and greatest ECMAScript everywhere__ _(via Babel)_ 55 | - __Provides a fast development workflow__ _(via hot-reloading on the client and server)_ 56 | - __Helps you write unit, functional, and end-to-end tests with ease__ _(via Jest and Puppeteer)_ 57 | - __Keeps your code clean and consistent__ _(via Prettier and ESLint)_ 58 | - __Gives you simple dev, test, build, and deploy scripts__ _(via NPM and bash)_ 59 | - __Runs on Node.js v6+__ _(via Babel runtime)_ 60 | 61 | # Documentation 62 | 63 | ## Install 64 | 65 | Clone the repository: 66 | 67 | ```bash 68 | git clone --depth=1 https://github.com/justinsisley/mostly.git your-project-name 69 | ``` 70 | 71 | Initialize your own repository: 72 | 73 | ```bash 74 | $ cd your-project-name 75 | $ rm -rf .git && git init 76 | ``` 77 | 78 | Install dependencies: 79 | 80 | ```bash 81 | $ npm install 82 | ``` 83 | 84 | ## Configuration 85 | 86 | Configurations for __Babel__, __ESLint__, __lint-staged__, and __prettier__ are contained within the `package.json` file. 87 | 88 | Configurations for __Webpack__ can be found in the `scripts/config` directory. 89 | 90 | Configuration for __CircleCI__ is contained in the `.circleci/config.yml` file. 91 | 92 | ## Develop 93 | 94 | Run the application in development mode: 95 | 96 | ```bash 97 | npm run dev 98 | ``` 99 | 100 | > __Note:__ The dev server runs on port _3320_ by default. Feel free to change it in `scripts/config/webpack.dev.js`. 101 | 102 | ## Test 103 | 104 | Run unit and functional tests: 105 | 106 | ```bash 107 | npm test 108 | ``` 109 | 110 | > __Note__: Both test suites will run concurrently. End-to-end tests will __not__ be run. 111 | 112 | ### Unit Tests 113 | 114 | Run unit tests: 115 | 116 | ```bash 117 | npm run test:unit 118 | ``` 119 | 120 | > __Note__: Jest will run any file that matches the following pattern: `src/**/__tests__/unit.js` 121 | 122 | ### Functional Tests (API endpoint tests) 123 | 124 | Run functional tests: 125 | 126 | ```bash 127 | npm run test:func 128 | ``` 129 | 130 | > __Note__: Jest will run any file that matches the following pattern: `src/**/__tests__/func.js` 131 | 132 | ### End-to-end Tests (user interface tests) 133 | 134 | Run end-to-end tests: 135 | 136 | ```bash 137 | npm run test:e2e 138 | ``` 139 | 140 | > __Note__: Jest/Puppeteer will run any file that matches the following pattern: `src/**/__tests__/e2e.js` 141 | 142 | ### Test Modes and Options 143 | 144 | Run a test suite in watch mode: 145 | 146 | ```bash 147 | npm run test:unit -- --watch 148 | # or 149 | npm run test:func -- --watch 150 | ``` 151 | 152 | > __Note__: This option is simply passing the `watch` option directly to Jest. End-to-end tests do __not__ support the `watch` option. 153 | 154 | Run the unit test suite and generate a coverage report: 155 | 156 | ```bash 157 | npm run test:unit -- --coverage 158 | ``` 159 | 160 | > __Note__: Like `watch`, this option is passing the `coverage` option directly to Jest. Functional and end-to-end tests do __not__ support the `coverage` option. 161 | 162 | ## Build 163 | 164 | Create a static build: 165 | 166 | ```bash 167 | npm run build 168 | ``` 169 | 170 | > __Note:__ This script will use Babel and Webpack to compile code within the `src` directory into a `dist` directory. 171 | 172 | ## Production 173 | 174 | Run the application in production mode: 175 | 176 | ```bash 177 | npm start 178 | ``` 179 | 180 | > __Note:__ This script requires you to first create a build by running `npm run build`. 181 | 182 | The production application will run on port _3325_ by default. If you'd like to run it on another port, use the `PORT` environment variable. For example: 183 | 184 | ```bash 185 | PORT=8080 npm start 186 | ``` 187 | 188 | ## Deployment 189 | 190 | Deploy to [now.sh](https://zeit.co/now): 191 | 192 | ```bash 193 | npm run deploy 194 | ``` 195 | 196 | > __Note:__ [now.sh](https://zeit.co/now) is one of the quickest and easiest ways to get your app deployed. It also offers a free plan. Nevertheless, like every other part of this starter kit, I encourage you to modify `deploy.sh` to suit your needs. 197 | 198 | ## Pre-commit Hook 199 | 200 | This starter kit is pre-configured with a [git pre-commit hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks), which will automatically clean up your staged code using [Prettier](https://prettier.io/) and [ESLint](https://eslint.org/), then execute your unit and functional tests. This is done using [lint-staged](https://github.com/okonet/lint-staged) and [husky](https://github.com/typicode/husky). 201 | 202 | You can modify the pre-commit workflow using the `lint-staged` property in `package.json` and the `scripts/precommit.sh` file. 203 | 204 | ## Continuous Delivery 205 | 206 | [CircleCI](https://circleci.com/) was chosen as the continuous integration provider because it's one of the more popular CI's in use across GitHub and it offers a free plan. 207 | 208 | No matter which CI you decide to use in the long run, the configuration is code-based, which should make it relatively easy to migrate if and when you decide to use another provider. 209 | 210 | For information about setting up your repository in CircleCI, [sign up for a free account](https://circleci.com/signup/), then check out the [documentation](https://circleci.com/docs/2.0/). 211 | 212 | Once you've set up your repository in CircleCI, you'll need to [get a token from now.sh](https://zeit.co/account/tokens) in order to automate your deployment. Once you've got your token, add it to your CircleCI project as an environment variable named `NOW_TOKEN`. 213 | 214 | Now, when you push new commits, CircleCI will run the `lint`, `test:unit`, `test:func`, and `test:e2e` scripts, then deploy your app to __now.sh__ with zero downtime using the `deploy` script. 215 | 216 | It'll be up to you to configure a custom domain, promote from staging to production, etc., but this configuration gets you most of the way there; mostly. 217 | 218 | # FAQ 219 | 220 | ### What's with the name? 221 | 222 | This project is the successor of another project, [__clear__](https://github.com/justinsisley/clear). I considered making this the `v2` of __clear__ but decided that because __mostly__ handles client-side web application development as well as server development, it would not be in line with __clear__'s [raison d'être](https://www.merriam-webster.com/dictionary/raison%20d'%C3%AAtre). 223 | 224 | It's still "clear", in this context meaning "straightforward", but there's some additional complexity, therefore it's __mostly__ clear. 225 | 226 | I told you [at the start of this](#features) that there's no magic, so try not to be too disappointed. 227 | 228 | ### I don't get the tagline. 229 | 230 | That's not a question, but ok. It's a line from the movie [Aliens](https://www.youtube.com/watch?v=B436avtEXzs) and was made humorous by [South Park](https://en.wikipedia.org/wiki/Cat_Orgy). 231 | 232 | ### Why are you promoting now.sh and CircleCI? Are you getting paid? 233 | 234 | No. I have no affiliation with either of those companies. In fact, at the time of first publishing this project, they're both new to me. 235 | 236 | __Bottom line:__ they're both free to use until you need to scale, and they're both very easy to work with. That puts them in perfect alignment with the purpose of this project. 237 | 238 | ### Why are you using Puppeteer instead of Nightwatch.js, TestCafe, Cypress, etc.? 239 | 240 | I've used several other end-to-end testing frameworks, and I've used them on one-person "teams", in small and medium-sized startups, and in large engineering organizations. In my experience, I've seen several themes repeat themselves: 241 | 242 | - Adding another test framework and/or assertion library does __not__ lend itself to increased velocity or a better developer experience. 243 | - Choosing a single assertion library for all types of tests, if possible, leads to higher-quality testing, as teams build cohesive expertise over time. 244 | - If tests aren't easy to write, they tend to be avoided*. 245 | - If tests are brittle, they tend to be avoided*. 246 | 247 | \*__Avoided__, in this context, means not written, not maintained, not trusted, not run regularly, or any combination thereof. 248 | 249 | Due to these experiences and observations, I've become a fan of __Jest__, as it contains the test runner, the assertion library, and the code coverage utility all in one easy-to-use package. 250 | 251 | I've also become a fan of __Puppeteer__, especially when combined with __jest-puppeteer__, as it allows me to easily automate Chrome while writing the exact same style of assertions I use for my unit and functional tests. 252 | 253 | Is __Puppeteer__ perfect? Of course not. For one, it's built on Chrome/Chromium, so you're not getting multi-browser testing like some other libraries. Nevertheless, the API is straightforward and easy-to-use, and when combined with __jest-puppeteer__, it creates a good enough developer experience that I end up writing a more complete end-to-end test suite, which ultimately boils down to a more trustworthy codebase and a more reliable product being delivered to the end user. 254 | 255 | ### What if there's a better, simpler library out there? Are you willing to switch to it? 256 | 257 | Absolutely. It's quite likely that there are better libraries than what this project is using, and if that's the case, please open an [issue](https://github.com/justinsisley/mostly/issues), or better yet, a [PR](https://github.com/justinsisley/mostly/pulls). 258 | 259 | If your suggestion truly improves and/or simplifies this project, there's a strong guarantee it will make the cut. 260 | 261 | ### Where's the Redux version? 262 | 263 | There's a [redux branch](https://github.com/justinsisley/mostly/tree/redux), but in addition to [redux](https://github.com/reduxjs/redux), it uses [react-redux](https://github.com/reduxjs/react-redux), [redux-thunk](https://github.com/reduxjs/redux-thunk), and [redux-actions](https://github.com/redux-utilities/redux-actions), so it's a bit more opinionated in library choice, architecture, and configuration. 264 | 265 | It's also configured to use [Redux DevTools](https://github.com/zalmoxisus/redux-devtools-extension), which can be quite useful during development and debugging. 266 | 267 | ### Why isn't there a CLI? Everything should have a CLI. 268 | 269 | For this project, I just don't think it's necessary. The [install steps](#install) are relatively simple, and three shell commands get you the most up-to-date starter kit checked out in a brand new repository. 270 | 271 | # Credits 272 |
Icon made by Eucalyp from www.flaticon.com is licensed by CC 3.0 BY
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mostly", 3 | "version": "1.0.0", 4 | "description": "They mostly come at night; mostly", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "dev": "sh ./scripts/dev.sh", 9 | "test": "sh ./scripts/test.sh", 10 | "test:unit": "sh ./scripts/testUnit.sh", 11 | "test:func": "sh ./scripts/testFunc.sh", 12 | "test:e2e": "sh ./scripts/testE2E.sh", 13 | "lint": "sh ./scripts/lint.sh", 14 | "build": "sh ./scripts/build.sh", 15 | "deploy": "sh ./scripts/deploy.sh", 16 | "start": "sh ./scripts/prod.sh", 17 | "clean": "sh ./scripts/clean.sh" 18 | }, 19 | "dependencies": { 20 | "@babel/runtime": "7.3.4", 21 | "axios": "0.18.0", 22 | "express": "4.16.4", 23 | "helmet": "3.15.1", 24 | "morgan": "1.9.1", 25 | "normalize.css": "8.0.1", 26 | "prop-types": "15.7.2", 27 | "react": "16.8.4", 28 | "react-dom": "16.8.4", 29 | "react-hot-loader": "4.8.0", 30 | "react-router-dom": "4.3.1" 31 | }, 32 | "devDependencies": { 33 | "@babel/cli": "7.2.3", 34 | "@babel/core": "7.3.4", 35 | "@babel/node": "7.2.2", 36 | "@babel/plugin-proposal-class-properties": "7.3.4", 37 | "@babel/plugin-syntax-dynamic-import": "7.2.0", 38 | "@babel/plugin-transform-runtime": "7.3.4", 39 | "@babel/preset-env": "7.3.4", 40 | "@babel/preset-react": "7.0.0", 41 | "@intervolga/optimize-cssnano-plugin": "1.0.6", 42 | "babel-core": "7.0.0-bridge.0", 43 | "babel-eslint": "10.0.1", 44 | "babel-loader": "8.0.5", 45 | "chokidar": "2.1.2", 46 | "concurrently": "4.1.0", 47 | "css-loader": "2.1.1", 48 | "eslint": "5.15.1", 49 | "eslint-config-airbnb": "17.1.0", 50 | "eslint-plugin-import": "2.16.0", 51 | "eslint-plugin-jsx-a11y": "6.2.1", 52 | "eslint-plugin-react": "7.12.4", 53 | "file-loader": "3.0.1", 54 | "html-webpack-plugin": "3.2.0", 55 | "husky": "1.3.1", 56 | "jest": "24.3.1", 57 | "jest-puppeteer": "4.0.0", 58 | "lint-staged": "8.1.5", 59 | "mini-css-extract-plugin": "0.5.0", 60 | "now": "14.0.3", 61 | "prettier": "1.16.4", 62 | "puppeteer": "1.13.0", 63 | "style-loader": "0.23.1", 64 | "webpack": "4.29.6", 65 | "webpack-cli": "3.2.3", 66 | "webpack-dev-server": "3.2.1" 67 | }, 68 | "babel": { 69 | "presets": [ 70 | "@babel/preset-env", 71 | "@babel/preset-react" 72 | ], 73 | "plugins": [ 74 | "@babel/plugin-proposal-class-properties", 75 | "@babel/plugin-syntax-dynamic-import", 76 | "@babel/plugin-transform-runtime", 77 | "react-hot-loader/babel" 78 | ] 79 | }, 80 | "eslintConfig": { 81 | "parser": "babel-eslint", 82 | "extends": "airbnb", 83 | "globals": { 84 | "beforeAll": true, 85 | "beforeEach": true, 86 | "describe": true, 87 | "document": true, 88 | "expect": true, 89 | "it": true, 90 | "jest": true, 91 | "page": true, 92 | "test": true, 93 | "window": true 94 | } 95 | }, 96 | "eslintIgnore": [ 97 | "coverage/**", 98 | "dist/**" 99 | ], 100 | "lint-staged": { 101 | "*.js": [ 102 | "prettier --write", 103 | "eslint --ignore-pattern /dist/ --fix ./", 104 | "git add" 105 | ], 106 | "*.{css,json}": [ 107 | "prettier --write", 108 | "git add" 109 | ] 110 | }, 111 | "prettier": { 112 | "singleQuote": true, 113 | "trailingComma": "all" 114 | }, 115 | "husky": { 116 | "hooks": { 117 | "pre-commit": "sh ./scripts/precommit.sh" 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | rm -rf ./dist 2 | 3 | NODE_ENV=production webpack \ 4 | --config ./scripts/config/webpack.prd.js \ 5 | --mode production \ 6 | ./src/client/index 7 | 8 | NODE_ENV=production babel \ 9 | ./src/server \ 10 | --out-dir ./dist/server \ 11 | --ignore '**/__tests__/**' -------------------------------------------------------------------------------- /scripts/clean.sh: -------------------------------------------------------------------------------- 1 | rm -rf coverage dist node_modules package-lock.json -------------------------------------------------------------------------------- /scripts/config/webpack.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const webpack = require('webpack'); 3 | const shared = require('./webpack.shared'); 4 | 5 | const config = { 6 | output: shared.output, 7 | module: { 8 | rules: shared.rules, 9 | }, 10 | plugins: [ 11 | new webpack.HotModuleReplacementPlugin(), 12 | shared.plugins.htmlWebPackPlugin, 13 | ], 14 | devServer: { 15 | hotOnly: true, 16 | inline: true, 17 | port: 3320, 18 | proxy: { 19 | '/api': 'http://localhost:3325', 20 | }, 21 | stats: shared.stats, 22 | }, 23 | resolve: shared.resolve, 24 | devtool: 'cheap-module-eval-source-map', 25 | }; 26 | 27 | module.exports = config; 28 | -------------------------------------------------------------------------------- /scripts/config/webpack.prd.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const OptimizeCssnanoPlugin = require('@intervolga/optimize-cssnano-plugin'); 4 | const shared = require('./webpack.shared'); 5 | 6 | const config = { 7 | output: shared.output, 8 | module: { 9 | rules: shared.rules, 10 | }, 11 | plugins: [ 12 | shared.plugins.htmlWebPackPlugin, 13 | new MiniCssExtractPlugin({ 14 | filename: '[name].css', 15 | chunkFilename: '[id].css', 16 | }), 17 | new OptimizeCssnanoPlugin({ 18 | cssnanoOptions: { 19 | preset: [ 20 | 'default', 21 | { 22 | discardComments: { 23 | removeAll: true, 24 | }, 25 | }, 26 | ], 27 | }, 28 | }), 29 | ], 30 | resolve: shared.resolve, 31 | stats: shared.stats, 32 | }; 33 | 34 | module.exports = config; 35 | -------------------------------------------------------------------------------- /scripts/config/webpack.shared.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const path = require('path'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const HtmlWebPackPlugin = require('html-webpack-plugin'); 5 | 6 | const clientDist = path.join(__dirname, '../../dist/client'); 7 | const clientIndexHtml = path.join(__dirname, '../../src/client/index.html'); 8 | const favicon = path.join(__dirname, '../../src/client/favicon.png'); 9 | 10 | const { NODE_ENV } = process.env; 11 | const isPrd = NODE_ENV === 'production'; 12 | const styleLoader = isPrd ? MiniCssExtractPlugin.loader : 'style-loader'; 13 | 14 | module.exports = { 15 | // Configuration for webpack.output 16 | output: { 17 | path: clientDist, 18 | publicPath: '/', 19 | }, 20 | 21 | // Configuration for webpack.module.rules 22 | rules: [ 23 | // Load JavaScript 24 | { 25 | test: /\.jsx?$/, 26 | exclude: /node_modules/, 27 | use: { 28 | loader: 'babel-loader', 29 | }, 30 | }, 31 | 32 | // Load global CSS files from node_modules 33 | { 34 | test: /\.css$/, 35 | include: /node_modules/, 36 | use: [styleLoader, 'css-loader'], 37 | }, 38 | 39 | // Load local CSS modules from src 40 | { 41 | test: /\.css$/, 42 | exclude: /node_modules/, 43 | use: [ 44 | styleLoader, 45 | { 46 | loader: 'css-loader', 47 | options: { 48 | modules: true, 49 | }, 50 | }, 51 | ], 52 | }, 53 | 54 | // Load static assets (images, fonts, etc.) 55 | { 56 | test: /\.(gif|png|jpe?g|ttf|eot|svg|woff|woff2)$/, 57 | use: { 58 | loader: 'file-loader', 59 | options: { 60 | name: 'assets/[hash].[ext]', 61 | }, 62 | }, 63 | }, 64 | ], 65 | 66 | // Configuration for webpack.plugins 67 | plugins: { 68 | htmlWebPackPlugin: new HtmlWebPackPlugin({ 69 | template: clientIndexHtml, 70 | filename: './index.html', 71 | minify: isPrd 72 | ? { 73 | collapseWhitespace: true, 74 | } 75 | : false, 76 | hash: isPrd, 77 | favicon, 78 | }), 79 | }, 80 | 81 | // Configuration for webpack.resolve 82 | resolve: { 83 | extensions: ['.js', '.jsx'], 84 | }, 85 | 86 | // Configuration for webpack.stats 87 | stats: { 88 | children: false, 89 | entrypoints: false, 90 | hash: false, 91 | modules: false, 92 | version: false, 93 | }, 94 | }; 95 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | # This value will determine your now.sh hostname 2 | # For example: https://{NOW_SUBDOMAIN}.now.sh 3 | # https://zeit.co/docs/getting-started/assign-a-domain-name 4 | NOW_SUBDOMAIN="mostly" 5 | 6 | # These values will determine how your application auto-scales 7 | # https://zeit.co/docs/getting-started/scaling 8 | MIN_INSTANCES=1 9 | MAX_INSTANCES=1 10 | 11 | # This value will determine the regions your application will deploy to 12 | # https://zeit.co/docs/features/scaling#scaling-while-deploying 13 | REGIONS="sfo" 14 | 15 | # 16 | # You generally won't need to change anything below this line unless you begin 17 | # using a custom domain name for your deployment 18 | # 19 | 20 | # Deploy and get the deployment ID 21 | NOW_DEPLOY_ID=$( now --public --no-clipboard --regions=$REGIONS --token=$NOW_TOKEN ) 22 | 23 | # Create an alias with the new deployment ID 24 | now alias $NOW_DEPLOY_ID "$NOW_SUBDOMAIN" --token=$NOW_TOKEN 25 | 26 | # Remove any unaliased deployments 27 | now rm $NOW_SUBDOMAIN --safe --yes --token=$NOW_TOKEN 28 | 29 | # Scale the deployment and always exit successfully 30 | now scale "$NOW_SUBDOMAIN.now.sh" $MIN_INSTANCES $MAX_INSTANCES --token=$NOW_TOKEN || exit 0 -------------------------------------------------------------------------------- /scripts/dev.sh: -------------------------------------------------------------------------------- 1 | concurrently --kill-others --raw "sh ./scripts/devServer.sh" "sh ./scripts/devClient.sh" -------------------------------------------------------------------------------- /scripts/devClient.sh: -------------------------------------------------------------------------------- 1 | webpack-dev-server \ 2 | --config ./scripts/config/webpack.dev.js \ 3 | --history-api-fallback \ 4 | --mode development \ 5 | ./src/client/index \ 6 | || exit 0 # Always exit successfully -------------------------------------------------------------------------------- /scripts/devServer.sh: -------------------------------------------------------------------------------- 1 | babel-node src/server/index.js \ 2 | || exit 0 # Always exit successfully -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | eslint --ignore-pattern /dist/ ./ -------------------------------------------------------------------------------- /scripts/precommit.sh: -------------------------------------------------------------------------------- 1 | lint-staged 2 | npm test -------------------------------------------------------------------------------- /scripts/prod.sh: -------------------------------------------------------------------------------- 1 | NODE_ENV=production node dist/server/index.js -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | concurrently --raw "npm run test:unit" "npm run test:func" -------------------------------------------------------------------------------- /scripts/testE2E.sh: -------------------------------------------------------------------------------- 1 | # Silently run the build script 2 | npm run build >/dev/null 3 | 4 | SERVER_COMMAND="npm start >/dev/null" 5 | TEST_COMMAND="jest src\/[^_]+\/__tests__\/e2e.js --config='{\"preset\": \"jest-puppeteer\"}'" 6 | 7 | # Make concurrently run the server and tests at the same time 8 | concurrently --kill-others --raw "$SERVER_COMMAND" "sleep 3 && $TEST_COMMAND" 9 | 10 | # Clean up after ourselves 11 | rm -rf dist 12 | 13 | exit 0 -------------------------------------------------------------------------------- /scripts/testFunc.sh: -------------------------------------------------------------------------------- 1 | SERVER_COMMAND="NODE_ENV=production babel-node src/server/index.js >/dev/null" 2 | TEST_COMMAND="jest src\/[^_]+\/__tests__\/func.js $1 --env=node" 3 | 4 | # Make concurrently run the server and tests at the same time 5 | concurrently --kill-others --raw "$SERVER_COMMAND" "sleep 3 && $TEST_COMMAND" 6 | 7 | exit 0 -------------------------------------------------------------------------------- /scripts/testUnit.sh: -------------------------------------------------------------------------------- 1 | jest src\/[^_]+\/__tests__\/unit.js $1 --collectCoverageFrom="src/{client,server}/**/*.js" --testURL="http://localhost" 2 | -------------------------------------------------------------------------------- /src/client/components/App/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { hot } from 'react-hot-loader'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { AuthProvider } from '../../contexts/auth'; 5 | import { RouterProvider } from '../../contexts/router'; 6 | import Router from '../Router'; 7 | 8 | function App() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | export default hot(module)(App); 21 | -------------------------------------------------------------------------------- /src/client/components/Dashboard/__tests__/e2e.js: -------------------------------------------------------------------------------- 1 | describe('Dashboard', () => { 2 | beforeAll(async () => { 3 | await page.goto('http://localhost:3325/login'); 4 | }); 5 | 6 | it('should navigate to the dashboard after login', async () => { 7 | await expect(page).toFill('#Username', 'ExampleUsername'); 8 | await expect(page).toClick('button', { text: 'Continue' }); 9 | await expect(page).toMatch('Logged in as ExampleUsername'); 10 | }); 11 | 12 | it('should display "Dashboard" text on page', async () => { 13 | await expect(page).toMatch('Dashboard'); 14 | }); 15 | 16 | it('should have a "Log Out" button', async () => { 17 | await expect(page).toMatchElement('button', { text: 'Log Out' }); 18 | }); 19 | 20 | it('should navigate to the login page on logout', async () => { 21 | await expect(page).toClick('button', { text: 'Log Out' }); 22 | await expect(page).toMatch('Log In'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/client/components/Dashboard/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Button } from '../Form'; 4 | import styles from './styles.css'; 5 | 6 | function Dashboard({ username, logOut }) { 7 | return ( 8 |
9 |
10 |

11 | Dashboard 12 |

13 | 14 |
15 | Logged in as 16 | {' '} 17 | 18 | {username} 19 | 20 |
21 | 22 | 25 |
26 |
27 | ); 28 | } 29 | 30 | Dashboard.propTypes = { 31 | logOut: PropTypes.func.isRequired, 32 | username: PropTypes.string.isRequired, 33 | }; 34 | 35 | export default Dashboard; 36 | -------------------------------------------------------------------------------- /src/client/components/Dashboard/styles.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | height: 100vh; 6 | } 7 | 8 | .card { 9 | width: 250px; 10 | } 11 | 12 | .message { 13 | margin-bottom: 25px; 14 | } 15 | -------------------------------------------------------------------------------- /src/client/components/Form/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './styles.css'; 4 | 5 | export function Button(props) { 6 | return ( 7 | 13 | ); 14 | } 15 | 16 | Button.propTypes = { 17 | children: PropTypes.node, 18 | className: PropTypes.string, 19 | onClick: PropTypes.func, 20 | }; 21 | 22 | Button.defaultProps = { 23 | children: null, 24 | className: '', 25 | onClick() {}, 26 | }; 27 | 28 | export function Input(props) { 29 | return ( 30 | 31 | 45 | 46 | ); 47 | } 48 | 49 | Input.propTypes = { 50 | className: PropTypes.string, 51 | error: PropTypes.bool, 52 | label: PropTypes.string, 53 | onChange: PropTypes.func, 54 | }; 55 | 56 | Input.defaultProps = { 57 | className: '', 58 | error: false, 59 | label: '', 60 | onChange() {}, 61 | }; 62 | -------------------------------------------------------------------------------- /src/client/components/Form/styles.css: -------------------------------------------------------------------------------- 1 | .input, 2 | .button { 3 | padding: 12px 15px; 4 | border-radius: 2px; 5 | font-size: 15px; 6 | } 7 | 8 | .button { 9 | min-width: 120px; 10 | border: 2px solid; 11 | border-color: #007bff; 12 | background-color: #007bff; 13 | color: #fff; 14 | } 15 | 16 | .label { 17 | display: block; 18 | margin-bottom: 20px; 19 | } 20 | 21 | .input { 22 | box-sizing: border-box; 23 | display: block; 24 | width: 100%; 25 | margin-top: 8px; 26 | padding: 12px 15px; 27 | border: 1px solid #ced4da; 28 | } 29 | 30 | .input.error { 31 | border-color: #dc3545; 32 | } 33 | 34 | .input.error:focus { 35 | border-color: #dc3545; 36 | box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.25); 37 | } 38 | 39 | .input:focus, 40 | .button:focus { 41 | border-color: transparent; 42 | outline: none; 43 | box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.5); 44 | } 45 | -------------------------------------------------------------------------------- /src/client/components/Login/__tests__/e2e.js: -------------------------------------------------------------------------------- 1 | describe('Login', () => { 2 | beforeAll(async () => { 3 | await page.goto('http://localhost:3325/login'); 4 | }); 5 | 6 | it('should display "Log In" text on page', async () => { 7 | await expect(page).toMatch('Log In'); 8 | }); 9 | 10 | it('should have a "Username" input field', async () => { 11 | await expect(page).toMatchElement('#Username'); 12 | }); 13 | 14 | it('should have a "Continue" button', async () => { 15 | await expect(page).toMatchElement('button', { text: 'Continue' }); 16 | }); 17 | 18 | it('should do nothing with invalid input', async () => { 19 | await expect(page).toClick('button', { text: 'Continue' }); 20 | await expect(page).toMatch('Log In'); 21 | }); 22 | 23 | it('should log in with valid input', async () => { 24 | await expect(page).toFill('#Username', 'ExampleUsername'); 25 | await expect(page).toClick('button', { text: 'Continue' }); 26 | await expect(page).not.toMatch('Log In'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/client/components/Login/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Redirect } from 'react-router-dom'; 4 | import { Button, Input } from '../Form'; 5 | import styles from './styles.css'; 6 | 7 | class Login extends React.Component { 8 | static propTypes = { 9 | loggedIn: PropTypes.bool.isRequired, 10 | logIn: PropTypes.func.isRequired, 11 | }; 12 | 13 | state = { 14 | username: '', 15 | error: false, 16 | }; 17 | 18 | onEmailChange = (e) => { 19 | this.setState({ 20 | username: e.target.value, 21 | error: false, 22 | }); 23 | }; 24 | 25 | onSubmit = (e) => { 26 | e.preventDefault(); 27 | 28 | const { logIn } = this.props; 29 | const { username } = this.state; 30 | 31 | if (!username) { 32 | this.setState({ error: true }); 33 | return; 34 | } 35 | 36 | logIn(username); 37 | }; 38 | 39 | render() { 40 | const { loggedIn } = this.props; 41 | const { error } = this.state; 42 | 43 | if (loggedIn) return ; 44 | 45 | return ( 46 |
47 |
48 |

49 | Log In 50 |

51 | 52 | 57 | 58 | 61 |
62 |
63 | ); 64 | } 65 | } 66 | 67 | export default Login; 68 | -------------------------------------------------------------------------------- /src/client/components/Login/styles.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | height: 100vh; 6 | } 7 | 8 | .form { 9 | width: 250px; 10 | } 11 | -------------------------------------------------------------------------------- /src/client/components/Router/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Redirect, Route, Switch } from 'react-router-dom'; 3 | import { withAuth, withoutAuth } from '../../contexts/auth'; 4 | import Dashboard from '../../containers/Dashboard'; 5 | import Login from '../../containers/Login'; 6 | 7 | const Router = () => ( 8 | 9 | 10 | 11 | 12 | {/* Redirect all unmatched routes to login */} 13 | 14 | 15 | ); 16 | 17 | export default Router; 18 | -------------------------------------------------------------------------------- /src/client/containers/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AuthConsumer } from '../contexts/auth'; 3 | import Dashboard from '../components/Dashboard'; 4 | 5 | export default () => ( 6 | 7 | {auth => } 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /src/client/containers/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AuthConsumer } from '../contexts/auth'; 3 | import Login from '../components/Login'; 4 | 5 | export default () => ( 6 | 7 | {auth => } 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /src/client/contexts/auth.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Redirect } from 'react-router-dom'; 4 | 5 | // Create a new context 6 | const AuthContext = React.createContext(); 7 | 8 | // Export the Consumer component 9 | export const AuthConsumer = AuthContext.Consumer; 10 | 11 | // Create and export a Provider component 12 | export class AuthProvider extends React.Component { 13 | static propTypes = { 14 | children: PropTypes.node, 15 | }; 16 | 17 | static defaultProps = { 18 | children: null, 19 | }; 20 | 21 | // Manage state like any other React Component 22 | state = { 23 | loggedIn: false, 24 | username: '', 25 | }; 26 | 27 | logIn = (username) => { 28 | this.setState({ 29 | loggedIn: true, 30 | username, 31 | }); 32 | }; 33 | 34 | logOut = () => { 35 | this.setState({ loggedIn: false }); 36 | }; 37 | 38 | render() { 39 | const { children } = this.props; 40 | 41 | return ( 42 | 50 | {children} 51 | 52 | ); 53 | } 54 | } 55 | 56 | // Higher-order component to require authenticated user 57 | export function withAuth(Component) { 58 | return function LoggedInComponent(props) { 59 | return ( 60 | 61 | {auth => (auth.loggedIn ? : ) 62 | } 63 | 64 | ); 65 | }; 66 | } 67 | 68 | // Higher-order component to require unauthenticated user 69 | export function withoutAuth(Component) { 70 | return function LoggedOutComponent(props) { 71 | return ( 72 | 73 | {auth => (auth.loggedIn 74 | ? 75 | : ) 76 | } 77 | 78 | ); 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /src/client/contexts/router.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withRouter } from 'react-router-dom'; 4 | 5 | // Create a new context with no default state 6 | const RouterContext = React.createContext(); 7 | 8 | // Export the Consumer component 9 | export const RouterConsumer = RouterContext.Consumer; 10 | 11 | // Create and export a Provider component 12 | function Provider(props) { 13 | const { children, ...rest } = props; 14 | 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | } 21 | 22 | Provider.propTypes = { 23 | children: PropTypes.node, 24 | }; 25 | 26 | Provider.defaultProps = { 27 | children: null, 28 | }; 29 | 30 | // Wrap the Provider in the `withRouter` higher-order component 31 | export const RouterProvider = withRouter(Provider); 32 | -------------------------------------------------------------------------------- /src/client/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinsisley/mostly/b14b40383ec9588847bf2ab446c204bbbe0ff0cc/src/client/favicon.png -------------------------------------------------------------------------------- /src/client/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 4 | } 5 | -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mostly 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/client/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/App'; 4 | 5 | // Global styles from NPM dependencies 6 | import 'normalize.css'; // eslint-disable-line import/first 7 | 8 | // Custom global styles 9 | import './index.css'; 10 | 11 | // Render the app 12 | ReactDOM.render(, document.getElementById('root')); 13 | -------------------------------------------------------------------------------- /src/server/api.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { catchErrors, handleNotFound } from './utils/errors'; 3 | import usersHandler from './controllers/users'; 4 | 5 | const router = Router(); 6 | 7 | // Example path for `/api/users` 8 | router.get('/users', catchErrors(usersHandler)); 9 | 10 | // Handle calls to non-existent API paths 11 | router.use('*', handleNotFound); 12 | 13 | export default router; 14 | -------------------------------------------------------------------------------- /src/server/constants/env.js: -------------------------------------------------------------------------------- 1 | const { NODE_ENV, PORT } = process.env; 2 | 3 | export const isProd = /pro?d/i.test(NODE_ENV); 4 | export const port = PORT || '3325'; 5 | -------------------------------------------------------------------------------- /src/server/controllers/users/__tests__/func.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | describe('users', () => { 4 | it('should return an array of users', async () => { 5 | const { data } = await axios.get('http://localhost:3325/api/users'); 6 | 7 | // Should be an array of user objects 8 | expect(Array.isArray(data)); 9 | expect(data.length).toBe(10); 10 | 11 | // Check the first user for known properties 12 | expect(data[0]).toMatchObject({ 13 | id: expect.any(Number), 14 | name: expect.any(String), 15 | username: expect.any(String), 16 | email: expect.any(String), 17 | address: expect.any(Object), 18 | phone: expect.any(String), 19 | website: expect.any(String), 20 | company: expect.any(Object), 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/server/controllers/users/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | async function usersHandler(req, res) { 4 | const { data } = await axios.get('https://jsonplaceholder.typicode.com/users'); 5 | 6 | res.json(data); 7 | } 8 | 9 | export default usersHandler; 10 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import express from 'express'; 3 | import helmet from 'helmet'; 4 | import morgan from 'morgan'; 5 | import api from './api'; 6 | import { isProd, port } from './constants/env'; 7 | import { handleErrors } from './utils/errors'; 8 | import hot from './utils/hot'; 9 | 10 | const app = express(); 11 | 12 | // Middleware 13 | app.use(helmet()); // Basic security for Express 14 | app.use(morgan(isProd ? 'common' : 'dev')); // Basic logging 15 | app.use(express.json()); // Parse JSON in request bodies 16 | 17 | if (isProd) { 18 | app.use('/api', api); // Handle all "/api" paths 19 | app.use(express.static('dist/client')); // Serve static assets for the client 20 | 21 | // Send index.html for all unhandled requests 22 | app.get('*', (req, res) => { 23 | res.sendFile(path.join(__dirname, '../client/index.html')); 24 | }); 25 | } else { 26 | // In non-production environments, handle API endpoints in a "hot-reloading" 27 | // friendly way by requiring a fresh `api.js` module on every API request. 28 | app.use('/api', (req, res, next) => { 29 | // eslint-disable-next-line global-require 30 | require('./api').default(req, res, next); 31 | }); 32 | 33 | // Start the "hot-reloading" watcher 34 | hot.start(); 35 | } 36 | 37 | // Global error handling for uncaught errors 38 | app.use(handleErrors); 39 | 40 | // Start the server 41 | app.listen(port); 42 | -------------------------------------------------------------------------------- /src/server/utils/errors/__tests__/unit.js: -------------------------------------------------------------------------------- 1 | import { catchErrors, handleErrors, handleNotFound } from '../index'; 2 | 3 | describe('errors', () => { 4 | it('should export a "catchErrors" middleware', () => { 5 | expect(typeof catchErrors).toBe('function'); 6 | 7 | const routeHandlerMock = jest.fn(); 8 | catchErrors(routeHandlerMock)('req', 'res', 'next'); 9 | expect(routeHandlerMock.mock.calls.length).toBe(1); 10 | 11 | const routeHandlerArgs = routeHandlerMock.mock.calls[0]; 12 | expect(routeHandlerArgs[0]).toBe('req'); 13 | expect(routeHandlerArgs[1]).toBe('res'); 14 | expect(routeHandlerArgs[2]).toBe('next'); 15 | }); 16 | 17 | it('should export a "handleErrors" middleware', () => { 18 | expect(typeof handleErrors).toBe('function'); 19 | 20 | const jsonMock = jest.fn(); 21 | const statusMock = jest.fn(() => ({ json: jsonMock })); 22 | const resMock = { status: statusMock }; 23 | 24 | handleErrors({}, {}, resMock); 25 | 26 | expect(statusMock.mock.calls.length).toBe(1); 27 | expect(statusMock.mock.calls[0][0]).toBe(500); 28 | 29 | expect(jsonMock.mock.calls.length).toBe(1); 30 | expect(jsonMock.mock.calls[0][0]).toMatchObject({}); 31 | }); 32 | 33 | it('should export a "handleNotFound" middleware', () => { 34 | expect(typeof handleNotFound).toBe('function'); 35 | 36 | const endMock = jest.fn(); 37 | const statusMock = jest.fn(() => ({ end: endMock })); 38 | const resMock = { status: statusMock }; 39 | 40 | handleNotFound({}, resMock); 41 | 42 | expect(statusMock.mock.calls.length).toBe(1); 43 | expect(statusMock.mock.calls[0][0]).toBe(404); 44 | 45 | expect(endMock.mock.calls.length).toBe(1); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/server/utils/errors/index.js: -------------------------------------------------------------------------------- 1 | // Wrapper function for route handlers that catches any uncaught errors 2 | export function catchErrors(fn) { 3 | return (req, res, next) => { 4 | Promise.resolve(fn(req, res, next)).catch(next); 5 | }; 6 | } 7 | 8 | // Middleware for handling the response for uncaught errors 9 | // eslint-disable-next-line no-unused-vars 10 | export function handleErrors(err, req, res, next) { 11 | if (err && err.stack) console.error(err.stack); 12 | 13 | res.status(500).json({}); 14 | } 15 | 16 | // Handler for non-existent API routes 17 | export function handleNotFound(req, res) { 18 | res.status(404).end(); 19 | } 20 | -------------------------------------------------------------------------------- /src/server/utils/hot/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | const serverDir = path.join(__dirname, '../../'); 4 | 5 | function start() { 6 | // Require chokidar just in time so it can be a devDependency 7 | const chokidar = require('chokidar'); // eslint-disable-line 8 | const watcher = chokidar.watch(serverDir); 9 | 10 | watcher.on('ready', () => { 11 | watcher.on('all', () => { 12 | console.log('Server module cache cleared'); 13 | 14 | Object.keys(require.cache).forEach((id) => { 15 | if (/[/\\]server[/\\]/.test(id)) delete require.cache[id]; 16 | }); 17 | }); 18 | }); 19 | } 20 | 21 | export default { 22 | start, 23 | }; 24 | --------------------------------------------------------------------------------