A Webpack2 boilerplate, partly based on this Egghead.io course; Using Webpack for Production JavaScript Applications
- ES2015/ES2016
- Node6 or Node7
- Npm as a task/build runner
- Webpack2 with tree-shaking and hot module replacement (HMR)
- Webpack DLL plugin for faster builds
- Load polyfills on demand using dynamic import
- Node Express middleware
- Linting with eslint and stylelint
- Unit tests with Mocha, Chai, Sinon and JsDom
- Integration tests with Node Express server
- Acceptance testing with WebdriverIO, Cucumber.js, and Node Express
- Code coverage and reporting with Istanbul
- SASS boilerplate with Solved by Flexbox Holy Grail example layout
- Self hosting Google Material Icons and Font Roboto
- Framework agnostic. No dependencies to frameworks like React or Angular
- Uses husky to prevent bad commits
This project uses husky to run scripts
before an actual git commit
More details about Husky can be found here:
start
: run Express sever with Hot Module Reloading (HMR), eslint and stylelint, serving files at http://localhost:8084test
: run unit tests and integration teststest:watch
: run unit tests in watch modetest:single
: run a single test file in watch mode, e.g.npm run test:single test/unit/logger/logger.spec.js
npm run test:single test/integration/server/server.spec.js
test:pattern
: will run tests and suites with names matching the given pattern, e.g.pattern=logger npm run test:pattern
will run only thelogger
testslint
: lint according to rules in.eslintrc
and.stylelintrc
analyze
: run webpack-bundle-size-analyzer to analyze the output bundle sizes
Note: There is aconsole.log
statement at the top of thewebpack.config
file that must be removed before this script can be runclean
: remove dist and coverage directorybuild
: bundle the app to the dist dir using development settingsbuild:prod
: bundle the app to the dist dir using production settingsserver
: run Express sever with the generated bundle, serving files at http://localhost:8000precommit
: husky run command for the git pre-commit hook
- Install Node6 or Node7 (via nvm)
- Clone this repository:
git clone https://github.com/leifoolsen/webpack2-boilerplate.git
(or download zip) - CD to project directory:
cd webpack2-boilerplate
- Remove existing git:
rm -rf .git
- Init your git:
git init
- Install dependencies:
npm install
oryarn install
- Build dll:
npm run build:dll
- Modify
package.json
, e.g.name, author, description, repository
- Add your own 3'rd party dependencies to
package.json
- Add those 3'rd party dependencies to
./src/vendor.js
Note: Remember to add your own repo to package.json
"repository": {
"type": "git",
"url": "https://github.com/<your-git>/<your-project>.git"
},
- Open a console (shell) and type:
npm start
- Open a browser at
http://localhost:8084
- Modify some SASS code, e.g. in
./src/stylesheets/base/_base.scss
a {
color: $brand-color;
text-decoration: none;
@include on-event {
color: $text-color;
text-decoration: underline;
}
}
- Change link color to green and save.
a {
color: green;
...
}
- Switch to browser
- All links should be green
- Click the
Ping
button and verify that the response is displayed with a date, e.g.2017-03-15 21:12:26: {"ping":"pong!"}
- Modify
./src/app/ping.js
const ping = el => {
request(apiPath)
.then(response => el.textContent =
`${moment().format('YYYY-MM-DD HH:mm:ss')}: ${JSON.stringify(response)}`)
.catch(err => el.textContent = err);
};
- Remove date,
YYYY-MM-DD
, from format and save
const ping = el => {
request(apiPath)
.then(response => el.textContent =
`${moment().format('HH:mm:ss')}: ${JSON.stringify(response)}`)
.catch(err => el.textContent = err);
};
- Switch to browser and click the
Ping
button - The response should be displayed without a date, e.g.
21:12:26: {"ping":"pong!"}
npm run build:prod
npm run server
- Open a browser at
http://localhost:8000
Add your 3'rd party dependencies to vendor.js
.
- See: OPTIMIZING WEBPACK FOR FASTER REACT BUILDS
- See: Optimizing Webpack build times and improving caching with DLL bundles
- See: Webpack Plugins we been keepin on the DLL
Add your polyfills to polyfill.js
- See: Code Splitting - Using import()
- See: WE DON'T NEED YOUR POLYFILLS!
- See: Polyfills: everything you ever wanted to know, or maybe a bit less
- See: Conditionally load multiple Polyfills using Webpack, Promises and Code Splitting
Tests are divided into three categories; unit tests, integration tests and acceptance tests. Unit tests and integration tests uses Moca as a test runner. The acceptance tests uses WebdriverIO as a test runner. Istanbul is used for code coverage and reporting.
The following libraries are used:
- Mocha
- Chai
- Sinon
- JsDom headless browser
- Istanbul
To run the unit tests type: npm run test:unit
The following libraries are used:
- Mocha
- Chai
- Supertest
- ExpressJS
- Istanbul
To run the ingtegration tests type: npm run test:it
The following libraries are used:
- WebdriverIO
- WDIO Selenium standalone service
- Selenium standalone
- Cucumber
- Chai
- ExpressJS
For now, the (standalone) acceptance tests must be run manually. The only way to ro run standalone acceptance tests on a CI server, is to use a headless browser like PhantomJS (I think). Unfortunatley I have so far had no success running acceptance tests using PhantomJS.
# npm install - just in case
npm install
# Make bundle
npm run build:prod
# Fetch actual Seleninum distro
NODE_TLS_REJECT_UNAUTHORIZED=0 ./node_modules/.bin/selenium-standalone install
# Run acceptance tests
npm run wdio
# Expected output
------------------------------------------------------------------
[chrome #0-0] Session ID: e0bf7b24-2bfa-4053-ad47-1e87d5fe409a
[chrome #0-0] Spec: ~/dev/webpack2-boilerplate/test/features/example.feature
[chrome #0-0] Running: chrome
[chrome #0-0]
[chrome #0-0] Title check
[chrome #0-0]
[chrome #0-0] Get the title of webpage
[chrome #0-0] ✓ I open the url "http://localhost:8082/"
[chrome #0-0] ✓ I expect the title of the page to be "Webpack2 Boilerplate"
[chrome #0-0]
[chrome #0-0] Click the Ping button
[chrome #0-0] ✓ I open the url "http://localhost:8082/"
[chrome #0-0] ✓ I click the Ping button
[chrome #0-0] ✓ I expect the response to be "pong!"
[chrome #0-0]
[chrome #0-0]
[chrome #0-0] 5 passing (5s)
Chromedriver
According to the WebdriverIO Get Stared guide, the Chromedriver standalone server is required for running Chrome browser tests on a local machine. On latest Ubuntu and OSX I have run the tests without installing the Chromedriver. So far I have not experienced any problems running the tests without the Chrome Driver. If you must install Chromedriver, instructions can be found e.g. here, here and here.
npm run build:prod
, then browse ./coverage/lcov-report/index.html
,
./coverage/unit/lcov-report/index.html
, ./coverage/integration/lcov-report/index.html
e2e tests are not implemented in this boilerplate, but basically they are equal to the integration tests. The main difference is that you use a proxy to connect to a "real" api server before running your client api tests. A sample e2e test using a proxy can be found in the ./test/integration/proxy directory.
To see it in action, run the test:proxy-example
script.
Read Hot Module Replacement - React, React Hot Loader Getting Started, Tree-shaking with webpack 2 and Babel 6 and http://andrewhfarmer.com/webpack-hmr-tutorial/.
The boilerplate may, with a few modifications, be used with React. More details can be found here and here.
# dependencies
npm i -S react
npm i -S react-dom
# devdependencies
npm i -D babel-preset-react
npm i -D react-hot-loader@next
npm i -D eslint-plugin-react
import 'moment';
import 'react';
import 'react-dom';
Add "react" to presets and "react-hot-loader/babel" to development plugins.
{
"presets": [
["env", {
"es2015": {
"modules": false
},
"targets": {
"browsers": ["last 2 versions", "ie >= 11"]
}
}],
"react",
"stage-0"
],
"plugins": [
// See: http://2ality.com/2015/12/webpack-tree-shaking.html
"transform-es2015-template-literals",
"transform-es2015-literals",
"transform-es2015-function-name",
"transform-es2015-arrow-functions",
"transform-es2015-block-scoped-functions",
"transform-es2015-classes",
"transform-es2015-object-super",
"transform-es2015-shorthand-properties",
"transform-es2015-computed-properties",
"transform-es2015-for-of",
"transform-es2015-sticky-regex",
"transform-es2015-unicode-regex",
"check-es2015-constants",
"transform-es2015-spread",
"transform-es2015-parameters",
"transform-es2015-destructuring",
"transform-es2015-block-scoping",
"transform-es2015-typeof-symbol",
["transform-regenerator", { "async": false, "asyncGenerators": false }]
],
"env": {
"development": {
"plugins": [
"react-hot-loader/babel"
]
},
"test": {
"plugins": [
"transform-es2015-modules-commonjs"
]
},
"production": {
}
}
}
Add "react" to "plugins"
{
"plugins": [
"compat", // Allow configuration of target browser/s (npm i -D eslint-plugin-compat)
"react" // React specific linting rules (npm i -D eslint-plugin-react)
],
}
Enable all of the rules that you would like to use
{
"rules": {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
}
}
Add 'react-hot-loader/patch'
app: (!isHot ? [] : [
'./webpack-public-path.js',
// Put react-hot-loader/patch before webpack-hot-middleware,
// see: https://github.com/gaearon/react-hot-loader/issues/243
'react-hot-loader/patch',
'webpack-hot-middleware/client',
]).concat([
'./styles.scss',
'./index.js'
]),
import React from 'react';
const superStyles = {
backgroundColor: 'green'
};
const App = () => (
<div style={superStyles}>
<h1>Hello React!</h1>
</div>
);
export default App;
import polyfill from './polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
import './config/config';
import logger, {LOG_LEVEL} from './logger/logger';
import App from './components/App';
import './styles.scss';
if(window) {
/**
* An event handler for the error event.
* When an error is thrown, the following arguments are passed to the function:
* @param msg The message associated with the error, e.g. “Uncaught ReferenceError: foo is not defined”
* @param url The URL of the script or document associated with the error, e.g. “/dist/app.js”
* @param lineNo The line number (if available)
* @param columnNo The column number (if available)
* @param error The Error object associated with this error (if available)
* @return {boolean}
* @see https://developer.mozilla.org/en/docs/Web/API/GlobalEventHandlers/onerror
* @see https://blog.sentry.io/2016/01/04/client-javascript-reporting-window-onerror.html
*/
window.onerror = function (msg, url, lineNo, columnNo, error) {
const err = error || {};
const detail = {
name: err.name || msg || '',
line: lineNo,
column: columnNo,
url: url || '',
stack: err.stack || 'See browser console for detail',
};
logger.remoteLogger.log(LOG_LEVEL.error, msg, detail);
return false;
};
/**
* Flush logger
*/
window.addEventListener('beforeunload', () => {
logger.debug('Before unload. Flushing remote logger');
logger.remoteLogger.flush();
});
}
// Add polyfills
polyfill().catch(err => {
logger.error(err);
});
// Start the app
if (module.hot) {
// AppContainer is a necessary wrapper component for HMR
const AppContainer = require('react-hot-loader').AppContainer;
const render = (Component) => {
ReactDOM.render(
<AppContainer>
<Component/>
</AppContainer>,
document.getElementById('app')
);
};
render(App);
// Hot Module Replacement API
module.hot.accept('./components/App', () => {
const NextApp = require('./components/App').default;
render(NextApp);
});
}
else {
ReactDOM.render(
<App/>,
document.getElementById('app')
);
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>React Webpack2</title>
</head>
<body>
<main id="app">
<!-- Display a message if JS has been disabled on the browser. -->
<noscript>If you're seeing this message, that means
<strong>JavaScript has been disabled on your browser</strong>,
please <strong>enable JS</strong> to make this app work.
</noscript>
</main>
</body>
</html>
Enjoy your React coding :-)