├── .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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
23 | Log Out
24 |
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 |
11 | {props.children}
12 |
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 |
35 | {props.label}
36 |
37 |
44 |
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 |
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 |
--------------------------------------------------------------------------------