Compare commits

...

13 Commits

Author SHA1 Message Date
Kayne Ruse eb370663d2 Tweaked some display 2021-07-29 00:37:23 +10:00
Kayne Ruse 462116d980 Fixed whitespace 2021-07-28 23:18:22 +10:00
Kayne Ruse af06ddc06d Working on password recovery 2021-07-28 23:01:41 +10:00
Kayne Ruse f937ee47db Removed universal-cookie, fixed package.json versioning 2021-07-26 05:05:39 +01:00
Kayne Ruse 6d0dd419ca Fixed #22 2021-07-26 13:48:08 +10:00
Kayne Ruse 2919467dff Fixed #23 2021-07-26 13:38:33 +10:00
Kayne Ruse 269caac88c Tweaked README.md 2021-07-25 20:35:34 +01:00
Kayne Ruse 69c297fa74 Stupid line endings 2021-07-25 19:22:17 +10:00
Kayne Ruse 0f538be3e5 Fixed #20 2021-07-25 18:53:14 +10:00
Kayne Ruse f85b6e8793 Fixed passwords 2021-07-25 18:52:46 +10:00
Kayne Ruse 2af9532930 Fixed deploy bug, properly 2021-07-24 14:01:20 +01:00
Kayne Ruse 191da50740 Deploy bug 2021-07-24 22:20:17 +10:00
Kayne Ruse f24c7990f6 Fixed line-endings 2021-07-24 20:23:18 +10:00
12 changed files with 721 additions and 563 deletions
+91 -89
View File
@@ -1,89 +1,91 @@
# MERN-template # MERN-template
A website template using the MERN stack. The primary technology involved is: A website template using the MERN stack. The primary technology involved is:
* React * React
* Nodejs * Nodejs
* MariaDB (with Sequelize) * MariaDB (with Sequelize)
* Docker (with docker-compose) * Docker (with docker-compose)
This template is designed to support the development of persistent browser based games (PBBGs), but it, and it's component microservices, can be used elsewhere. This template is designed to support the development of persistent browser based games (PBBGs), but it, and it's component microservices, can be used elsewhere.
This template is released under the zlib license (see LICENSE). This template is released under the zlib license (see LICENSE).
See the [github wiki](https://github.com/krgamestudios/MERN-template/wiki) for full documentation. See the [github wiki](https://github.com/krgamestudios/MERN-template/wiki) for full documentation.
# Microservices # Microservices
There are external components to this template referred to as "microservices". These can be omitted entirely by simply removing the React components that access them. These are also available via [docker hub](https://hub.docker.com/u/krgamestudios). There are external components to this template referred to as "microservices". These can be omitted entirely by simply removing the React components that access them. These are also available via [docker hub](https://hub.docker.com/u/krgamestudios).
* News Server: https://github.com/krgamestudios/news-server * News Server: https://github.com/krgamestudios/news-server
* Auth Server: https://github.com/krgamestudios/auth-server * Auth Server: https://github.com/krgamestudios/auth-server
* Chat Server: https://github.com/krgamestudios/chat-server * Chat Server: https://github.com/krgamestudios/chat-server
# Setup Deployment # Setup Deployment
A clean install is this easy: A clean install is this easy:
``` ```
git clone https://github.com/krgamestudios/MERN-template.git git clone https://github.com/krgamestudios/MERN-template.git
node configure-script.js cd MERN-template
docker-compose up --build npm install
``` node configure-script.js
docker-compose up --build
# Setup Development ```
To set up this template in development mode: # Setup Development
1. Ensure mariadb is running in your development environment To set up this template in development mode:
2. Run `mariadb sql/create_database.sql` as the root user
3. Run `npm install` 1. Ensure mariadb is running in your development environment
4. Run `cp .envdev .env` and enter your details into the `.env` file 2. Run `mariadb sql/create_database.sql` as the root user
5. Execute `npm run dev` 3. Run `npm install`
6. Navigate to `http://localhost:3001` in your web browser 4. Run `cp .envdev .env` and enter your details into the `.env` file
5. Execute `npm run dev`
# Features List 6. Navigate to `http://localhost:3001` in your web browser
- Mainly one language across the codebase (JavaScript) # Features List
- Full documentation
- Setup tutorial - Mainly one language across the codebase (JavaScript)
- Fully Featured Account System (as a microservice) - Full documentation
- Email validation - Setup tutorial
- Logging in and out - Fully Featured Account System (as a microservice)
- Account deletion - Email validation
- Password management - Logging in and out
- JSON web token authentication - Account deletion
- Fully Featured News Blog (as a microservice) - Password management
- Publish, edit or delete articles as needed - JSON web token authentication
- Secured via admin panel - Fully Featured News Blog (as a microservice)
- Fully Featured Chat System (as a microservice) - Publish, edit or delete articles as needed
- Available when logged in - Secured via admin panel
- Chat logs saved to the database - Fully Featured Chat System (as a microservice)
- Room-based chat (type `/room name` to access a specific room) - Available when logged in
- Moderation tools - Chat logs saved to the database
- Permanently banning users - Room-based chat (type `/room name` to access a specific room)
- Chat-muting users for a time period - Moderation tools
- Users reporting offensive chat-content - Permanently banning users
- Easy To Use Configuration Script - Chat-muting users for a time period
- Sets up everything via docker - Users reporting offensive chat-content
- A default admin account (if desired) - Easy To Use Configuration Script
- Sets up everything via docker
# Coming Soon - A default admin account (if desired)
- Full documentation # Coming Soon
- Modding tutorials
- Full documentation
# Coming Eventually - Modding tutorials
- Fully Featured News Blog (as a microservice) # Coming Eventually
- Restore deleted articles
- Undo edits - Fully Featured News Blog (as a microservice)
- Fully Featured Chat System (as a microservice) - Restore deleted articles
- Custom emoji - Undo edits
- Private messaging - Fully Featured Chat System (as a microservice)
- Broadcasting to all channels - Custom emoji
- Badges next to usernames - Private messaging
- Better compression for client files - Broadcasting to all channels
- Backend for leaderboards (modding tutorial?) - Badges next to usernames
- Backend for energy systems (modding tutorial?) - Better compression for client files
- Backend for items, shops, trading and currency - Backend for leaderboards (modding tutorial?)
- Backend for energy systems (modding tutorial?)
- Backend for items, shops, trading and currency
+3
View File
@@ -29,6 +29,9 @@ const App = props => {
<LazyRoute path='/login' component={() => import('./pages/login')} /> <LazyRoute path='/login' component={() => import('./pages/login')} />
<LazyRoute path='/account' component={() => import('./pages/account')} /> <LazyRoute path='/account' component={() => import('./pages/account')} />
<LazyRoute path='/recover' component={() => import('./pages/recover')} />
<LazyRoute path='/reset' component={() => import('./pages/reset')} />
<LazyRoute path='/admin' component={() => import('./pages/admin')} /> <LazyRoute path='/admin' component={() => import('./pages/admin')} />
<LazyRoute path='/mod' component={() => import('./pages/mod')} /> <LazyRoute path='/mod' component={() => import('./pages/mod')} />
+91
View File
@@ -0,0 +1,91 @@
import React, { useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
import { TokenContext } from '../utilities/token-provider';
//utilities
const validateEmail = require('../../../common/utilities/validate-email');
const Recover = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (authTokens.accessToken) {
return <Redirect to='/' />;
}
//refs
const emailRef = useRef();
const recoverRef = useRef();
return (
<div className='page'>
<h1 className='centered'>Recover Password</h1>
<form className='constricted' onSubmit={
async evt => { //on submit
recoverRef.current.disabled = true;
evt.preventDefault();
const [result, redirect] = await handleSubmit(emailRef.current.value);
if (result) {
alert(result);
recoverRef.current.disabled = false;
}
//redirect
if (redirect) {
props.history.push('/');
}
}
}>
<div>
<label htmlFor='email'>Enter Your Email:</label>
<input type='email' name='email' ref={emailRef} />
</div>
<button type='submit' ref={recoverRef}>Recover Password</button>
</form>
</div>
);
};
const handleSubmit = async (email) => {
email = email.trim();
const err = handleValidation(email);
if (err) {
return [err];
}
//send to the auth server
const result = await fetch(`${process.env.AUTH_URI}/auth/recover`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
email
})
});
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.error(err);
return [err, false];
}
return [await result.text(), true];
};
//returns an error message, or null on success
const handleValidation = (email) => {
if (!validateEmail(email)) {
return 'invalid email';
}
return null;
};
export default Recover;
+89
View File
@@ -0,0 +1,89 @@
import React, { useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
import queryString from 'query-string';
import { TokenContext } from '../utilities/token-provider';
const Reset = props => {
//context
const authTokens = useContext(TokenContext);
//query
const query = queryString.parse(props.location.search);
//misplaced?
if (authTokens.accessToken || !query.email || !query.token) {
return <Redirect to='/' />;
}
//refs
const passwordRef = useRef();
const retypeRef = useRef();
const resetRef = useRef();
//render the thing
return (
<div className='page'>
<h1 className='centered'>Reset Password</h1>
<form className='constricted' onSubmit={async evt => {
evt.preventDefault();
const [err] = await update(passwordRef.current.value, retypeRef.current.value, query);
if (err) {
alert(err);
return;
}
alert('Details updated');
//redirect
if (redirect) {
props.history.push('/');
}
}}>
<div>
<div>
<label htmlFor='password'>Enter New Password:</label>
<input type='password' name='password' ref={passwordRef} />
</div>
<div>
<label htmlFor='retype'>Retype New Password:</label>
<input type='password' name='retype' ref={retypeRef} />
</div>
</div>
<button type='submit'>Update Information</button>
</form>
</div>
);
};
const update = async (password, retype, query) => {
if (password != retype) {
return ['Passwords do not match'];
}
if (password && password.length < 8) {
return ['Password is too short'];
}
const result = await fetch(`${process.env.AUTH_URI}/auth/reset?email=${query.email}&token=${query.token}`, {
method: 'PATCH',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
body: JSON.stringify({
password: password ? password : null,
})
});
if (!result.ok) {
return [`${await result.status}: ${await result.text()}`];
} else {
return [null];
}
}
export default Reset;
+4 -1
View File
@@ -22,16 +22,19 @@ const SignUp = props => {
const passwordRef = useRef(); const passwordRef = useRef();
const retypeRef = useRef(); const retypeRef = useRef();
const contactRef = useRef(); const contactRef = useRef();
const signupRef = useRef();
return ( return (
<div className='page'> <div className='page'>
<h1 className='centered'>Signup</h1> <h1 className='centered'>Signup</h1>
<form className='constricted' onSubmit={ <form className='constricted' onSubmit={
async evt => { //on submit async evt => { //on submit
signupRef.current.disabled = true;
evt.preventDefault(); evt.preventDefault();
const [result, redirect] = await handleSubmit(emailRef.current.value, usernameRef.current.value, passwordRef.current.value, retypeRef.current.value, contactRef.current.checked); const [result, redirect] = await handleSubmit(emailRef.current.value, usernameRef.current.value, passwordRef.current.value, retypeRef.current.value, contactRef.current.checked);
if (result) { if (result) {
alert(result); alert(result);
signupRef.current.disabled = false;
} }
//redirect //redirect
@@ -65,7 +68,7 @@ const SignUp = props => {
<input type='checkbox' name='contact' ref={contactRef} /> <input type='checkbox' name='contact' ref={contactRef} />
</div> </div>
<button type='submit'>Signup</button> <button type='submit' ref={signupRef}>Signup</button>
</form> </form>
</div> </div>
); );
+2
View File
@@ -9,6 +9,8 @@ const Visitor = () => {
<Link to='/signup'>Sign Up</Link> <Link to='/signup'>Sign Up</Link>
<span> - </span> <span> - </span>
<Link to='/login'>Log In</Link> <Link to='/login'>Log In</Link>
<span> - </span>
<Link to='/recover'>Recover</Link>
</div> </div>
); );
}; };
+15 -1
View File
@@ -26,7 +26,21 @@ const NewsFeed = props => {
return ( return (
<div> <div>
<h1 className='centered'>News Feed</h1> <h1 className='centered'>News Feed</h1>
{articles.map((article, index) => { {(articles || []).map((article, index) => {
//BUGFIX: check for empty data
if (!article.title) {
return article.title = '';
}
if (!article.author) {
return article.author = '';
}
if (!article.body) {
return article.body = '';
}
//render
return ( return (
<div key={index}> <div key={index}>
<hr /> <hr />
+8 -4
View File
@@ -57,13 +57,14 @@ See https://github.com/krgamestudios/MERN-template/wiki for help.
const newsName = await question('News Name', 'news'); const newsName = await question('News Name', 'news');
const newsWebAddress = await question('News Web Address', `${newsName}.${projectWebAddress}`); const newsWebAddress = await question('News Web Address', `${newsName}.${projectWebAddress}`);
const newsDBUser = await question('News DB Username', newsName); const newsDBUser = await question('News DB Username', newsName);
const newsDBPass = await question('News DB Password', 'charizard'); const newsDBPass = await question('News DB Password', 'venusaur');
//auth configuration //auth configuration
const authName = await question('Auth Name', 'auth'); const authName = await question('Auth Name', 'auth');
const authWebAddress = await question('Auth Web Address', `${authName}.${projectWebAddress}`); const authWebAddress = await question('Auth Web Address', `${authName}.${projectWebAddress}`);
const authResetAddress = await question('Auth Reset Addr', `${projectWebAddress}/reset`);
const authDBUser = await question('Auth DB Username', authName); const authDBUser = await question('Auth DB Username', authName);
const authDBPass = await question('Auth DB Password', 'venusaur'); const authDBPass = await question('Auth DB Password', 'charizard');
const emailSMTP = await question('Email SMTP', 'smtp.example.com'); const emailSMTP = await question('Email SMTP', 'smtp.example.com');
const emailUser = await question('Email Address', 'foobar@example.com'); const emailUser = await question('Email Address', 'foobar@example.com');
@@ -179,6 +180,7 @@ services:
environment: environment:
- WEB_PROTOCOL=https - WEB_PROTOCOL=https
- WEB_ADDRESS=${authWebAddress} - WEB_ADDRESS=${authWebAddress}
- WEB_RESET_ADDRESS=${authResetAddress}
- WEB_PORT=${authPort} - WEB_PORT=${authPort}
- DB_HOSTNAME=database - DB_HOSTNAME=database
- DB_DATABASE=${authName} - DB_DATABASE=${authName}
@@ -265,10 +267,12 @@ networks:
const dockerfile = ` const dockerfile = `
FROM node:15 FROM node:15
WORKDIR "/app" WORKDIR "/app"
COPY package*.json ./
RUN npm install
COPY . /app COPY . /app
RUN mkdir /app/public
RUN chown node:node /app/public
RUN npm install --production
EXPOSE ${projectPort} EXPOSE ${projectPort}
USER node
ENTRYPOINT ["bash", "-c"] ENTRYPOINT ["bash", "-c"]
CMD ["sleep 10 && npm start"] CMD ["sleep 10 && npm start"]
`; `;
+240 -289
View File
File diff suppressed because it is too large Load Diff
+61 -61
View File
@@ -1,61 +1,61 @@
{ {
"name": "mern-template", "name": "mern-template",
"version": "1.0.2", "version": "1.0.2",
"description": "A website template using the MERN stack.", "description": "A website template using the MERN stack.",
"main": "server/server.js", "main": "server/server.js",
"scripts": { "scripts": {
"start": "npm run build && node server/server.js", "start": "npm run build && node server/server.js",
"build": "npm run build:server && npm run build:client", "build": "npm run build:server && npm run build:client",
"build:server": "exit 0", "build:server": "exit 0",
"build:client": "webpack --env=production --config webpack.config.js", "build:client": "webpack --env=production --config webpack.config.js",
"dev": "concurrently npm:watch:server npm:watch:client", "dev": "concurrently npm:watch:server npm:watch:client",
"watch:server": "nodemon ./* --ext js,jsx,json --ignore 'node_modules/*'", "watch:server": "nodemon ./* --ext js,jsx,json --ignore 'node_modules/*'",
"watch:client": "webpack serve --env=development --config webpack.config.js", "watch:client": "webpack serve --env=development --config webpack.config.js",
"analyzer": "webpack --env=production --analyzer --config webpack.config.js" "analyzer": "webpack --env=production --analyzer --config webpack.config.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/KRGameStudios/MERN-template.git" "url": "git+https://github.com/KRGameStudios/MERN-template.git"
}, },
"author": "Kayne Ruse", "author": "Kayne Ruse",
"license": "ISC", "license": "ISC",
"bugs": { "bugs": {
"url": "https://github.com/KRGameStudios/MERN-template/issues" "url": "https://github.com/KRGameStudios/MERN-template/issues"
}, },
"homepage": "https://github.com/KRGameStudios/MERN-template#readme", "homepage": "https://github.com/KRGameStudios/MERN-template#readme",
"dependencies": { "dependencies": {
"@babel/core": ">=7.12.10", "@babel/core": "^7.14.8",
"@babel/preset-env": ">=7.12.11", "@babel/preset-env": "^7.14.8",
"@babel/preset-react": ">=7.12.10", "@babel/preset-react": "^7.14.5",
"@loadable/component": ">=5.14.1", "@loadable/component": "^5.15.0",
"babel-loader": ">=8.2.2", "babel-loader": "^8.2.2",
"clean-webpack-plugin": ">=3.0.0", "clean-webpack-plugin": "^3.0.0",
"concurrently": ">=5.3.0", "concurrently": "^6.2.0",
"css-loader": ">=5.1.3", "css-loader": "^6.2.0",
"dateformat": ">=4.5.1", "dateformat": "^4.5.1",
"dotenv": ">=8.2.0", "dotenv": "^10.0.0",
"express": ">=4.17.1", "express": "^4.17.1",
"html-webpack-plugin": ">=5.0.0-alpha.14", "html-webpack-plugin": "^5.3.2",
"jwt-decode": ">=3.1.2", "jwt-decode": "^3.1.2",
"mariadb": ">=2.5.2", "mariadb": "^2.5.4",
"raw-loader": ">=4.0.2", "query-string": "^7.0.1",
"react": ">=17.0.1", "raw-loader": "^4.0.2",
"react-dom": ">=17.0.1", "react": "^17.0.2",
"react-dropdown-select": ">=4.7.4", "react-dom": "^17.0.2",
"react-markdown": ">=5.0.3", "react-dropdown-select": "^4.7.4",
"react-router": ">=5.2.0", "react-markdown": "^6.0.2",
"react-router-dom": ">=5.2.0", "react-router": "^5.2.0",
"rehype-raw": "^5.1.0", "react-router-dom": "^5.2.0",
"sequelize": ">=6.4.0", "rehype-raw": "^5.1.0",
"socket.io-client": ">=4.0.0", "sequelize": "^6.6.5",
"style-loader": ">=2.0.0", "socket.io-client": "^4.1.3",
"universal-cookie": ">=4.0.4", "style-loader": "^3.2.1",
"webpack": ">=5.15.0", "webpack": "^5.46.0",
"webpack-cli": ">=4.3.1" "webpack-bundle-analyzer": "^4.4.2",
}, "webpack-cli": "^4.7.2"
"devDependencies": { },
"nodemon": ">=2.0.7", "devDependencies": {
"webpack-bundle-analyzer": ">=4.3.0", "nodemon": "^2.0.12",
"webpack-dev-server": ">=1.16.5" "webpack-dev-server": "^3.11.2"
} }
} }
+1 -2
View File
@@ -8,10 +8,9 @@ const path = require('path');
const express = require('express'); const express = require('express');
const app = express(); const app = express();
const server = require('http').Server(app); const server = require('http').Server(app);
const bodyParser = require('body-parser');
//config //config
app.use(bodyParser.json()); app.use(express.json());
//database connection //database connection
const database = require('./database'); const database = require('./database');
+116 -116
View File
@@ -1,116 +1,116 @@
//plugins //plugins
const { DefinePlugin } = require('webpack'); const { DefinePlugin } = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
//libraries //libraries
const path = require('path'); const path = require('path');
//the exported config function //the exported config function
module.exports = ({ production, analyzer }) => { module.exports = ({ production, analyzer }) => {
return { return {
mode: production ? "production" : "development", mode: production ? "production" : "development",
entry: path.resolve(__dirname, 'client', 'client.jsx'), entry: path.resolve(__dirname, 'client', 'client.jsx'),
output: { output: {
path: path.resolve(__dirname, 'public'), path: path.resolve(__dirname, 'public'),
filename: '[name].[chunkhash].js', filename: '[name].[chunkhash].js',
sourceMapFilename: '[name].[chunkhash].js.map' sourceMapFilename: '[name].[chunkhash].js.map'
}, },
devtool: production ? 'source-map' : 'eval-source-map', devtool: production ? 'source-map' : 'eval-source-map',
resolve: { resolve: {
extensions: ['.js', '.jsx'] extensions: ['.js', '.jsx']
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.(js|jsx)$/, test: /\.(js|jsx)$/,
exclude: /(node_modules)/, exclude: /(node_modules)/,
use: [ use: [
{ {
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
presets: ['@babel/preset-env', '@babel/preset-react'], presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['@babel/plugin-syntax-dynamic-import'] plugins: ['@babel/plugin-syntax-dynamic-import']
} }
} }
] ]
}, },
{ {
test: /\.(css)$/, test: /\.(css)$/,
use: ['style-loader', 'css-loader'] use: ['style-loader', 'css-loader']
}, },
{ {
test: /\.(md)$/, test: /\.(md)$/,
use: [ use: [
{ {
loader: 'raw-loader' loader: 'raw-loader'
}, },
], ],
}, },
] ]
}, },
plugins: [ plugins: [
new DefinePlugin({ new DefinePlugin({
'process.env': { 'process.env': {
'PRODUCTION': production, 'PRODUCTION': production,
'NEWS_URI': production ? `"${process.env.NEWS_URI}"` : '"https://dev-news.krgamestudios.com"', 'NEWS_URI': production ? `"${process.env.NEWS_URI}"` : '"https://dev-news.krgamestudios.com"',
'AUTH_URI': production ? `"${process.env.AUTH_URI}"` : '"https://dev-auth.krgamestudios.com"', 'AUTH_URI': production ? `"${process.env.AUTH_URI}"` : '"https://dev-auth.krgamestudios.com"',
'CHAT_URI': production ? `"${process.env.CHAT_URI}"` : '"https://dev-chat.krgamestudios.com"', 'CHAT_URI': production ? `"${process.env.CHAT_URI}"` : '"https://dev-chat.krgamestudios.com"',
} }
}), }),
new CleanWebpackPlugin({ new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['*', '!content*'] cleanOnceBeforeBuildPatterns: ['*', '!content*']
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: './client/template.html', template: './client/template.html',
minify: { minify: {
collapseWhitespace: production, collapseWhitespace: production,
removeComments: production, removeComments: production,
removeAttributeQuotes: production removeAttributeQuotes: production
} }
}), }),
new BundleAnalyzerPlugin({ new BundleAnalyzerPlugin({
analyzerMode: analyzer ? 'server' : 'disabled' analyzerMode: analyzer ? 'server' : 'disabled'
}) })
], ],
devServer: { devServer: {
contentBase: path.resolve(__dirname, 'public'), contentBase: path.resolve(__dirname, 'public'),
compress: true, compress: true,
port: 3001, port: 3001,
proxy: { proxy: {
'/api/': 'http://localhost:3000/' '/api/': 'http://localhost:3000/'
}, },
overlay: { overlay: {
errors: true errors: true
}, },
stats: { stats: {
colors: true, colors: true,
hash: false, hash: false,
version: false, version: false,
timings: false, timings: false,
assets: false, assets: false,
chunks: false, chunks: false,
modules: false, modules: false,
reasons: false, reasons: false,
children: false, children: false,
source: false, source: false,
errors: true, errors: true,
errorDetails: false, errorDetails: false,
warnings: true, warnings: true,
publicPath: false publicPath: false
}, },
host: '0.0.0.0', host: '0.0.0.0',
disableHostCheck: true, disableHostCheck: true,
clientLogLevel: 'silent', clientLogLevel: 'silent',
historyApiFallback: true, historyApiFallback: true,
hot: true, hot: true,
injectHot: true injectHot: true
}, },
watchOptions: { watchOptions: {
ignored: /(node_modules)/ ignored: /(node_modules)/
} }
} }
}; };