Table of Contents
So far we explored setting our project using Webpack Dev Server’s Command line Interface (CLI) Let’s explore the other two options and create scripts to run them.
Three ways of setting up Webpack
Edit package.json ‘scripts’ tag and add start script for each setup:
./package.json
"scripts": { "start-cli": "webpack-dev-server --hot", "start-api": "babel-node server-api.js", "start-middleware": "node server-middleware.js", "clean": "rm -rf ./dist", "lint": "eslint .", "build-dev": "webpack --mode development", "build-prod": "webpack --mode production" }
What we just did:
– We added ‘start-cli’ script to replace ‘start’ script and to run Webpack-dev-server with Command Line Interface (the setup that we did so far)
– we added ‘start-api’ to run the project using Webpack, and Webpack-dev-server API instead.
– we added ‘startup-middleware’ to use Webpack-Dev-Server middleware.
Command line Interface (CLI)
We already have this set up and we changed the ‘start’ script tag to be ‘start-cli’ instead, but everything else is the same, but to enhance the functionality (like been able to parse scss) we will pass a config file.
./package.json
... "scripts": { "start-cli": "webpack-dev-server --hot --history-api-fallback --config webpack.cli.config.js", ...
and let’s create the cli config and since it will be the exact copy of webpack.config.js you could just rename webpack.config.js the file to be webpack.cli.config.js
./webpack.cli.config.js
const webpack = require('webpack'); module.exports = { mode: 'development', entry: [ '@babel/polyfill', './src/index.js' ], output: { filename: '[name]-bundle.js', publicPath: '/dist', }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: "babel-loader" } }, // SCSS { test: /\.scss$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true, importLoaders: 2, localIdentName: '[folder]-[local]', sourceMap: true } }, { loader: 'postcss-loader', options: { plugins: () => [require('autoprefixer')()], sourceMap: true }, }, { loader: 'sass-loader', options: { outputStyle: 'expanded', sourceMap: true } } ], }, // images { test: /\.(png|jp(e*)g|svg)$/, use: [{ loader: 'url-loader', options: { limit: 8000, // Convert images < 8kb to base64 strings name: 'images/[hash]-[name].[ext]' } }] }, //File loader used to load fonts { test: /\.(woff|woff2|eot|ttf|otf)$/, use: ['file-loader'] } ] } };
Give it a try by running yarn start-cli
and everything will work like before.
Webpack Dev Server API
Create server-api.js with the following contents:
./server-api.js
const WebpackDevServer = require('webpack-dev-server'); const webpack = require('webpack'); const config = require('./webpack.api.config.js'); const compiler = webpack(config); const server = new WebpackDevServer(compiler, { hot: true, publicPath: config.output.publicPath }); server.listen(8080, 'localhost', function() {});
What we just did is pretty simple and it’s the same that we did for CLI but just using the APIs insted:
– we added Webpack-dev-server instance (line 1) and then we configured it with ‘hot: true’ param to enagle HMR.
– and ‘publicPath: config.output.publicPath’ which will take the parameter from the config file, and pass it to the dev server.
– we also added Webpack (line 2) which will be used as a compiler for Webpack-dev-server (line 6)
Also we have to pass a few extra parameters to webpack.config.js so let’s create a new config file, and name it:
./webpack.api.config.js
const webpack = require('webpack'); module.exports = { mode: 'development', entry: [ '@babel/polyfill', './src/index.js', 'webpack/hot/dev-server', 'webpack-dev-server/client?http://localhost:8080/', ], output: { filename: '[name]-bundle.js', publicPath: '/dist', }, plugins: [ new webpack.HotModuleReplacementPlugin(), ], module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: "babel-loader" } }, // SCSS { test: /\.scss$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true, importLoaders: 2, localIdentName: '[folder]-[local]', sourceMap: true } }, { loader: 'postcss-loader', options: { plugins: () => [require('autoprefixer')()], sourceMap: true }, }, { loader: 'sass-loader', options: { outputStyle: 'expanded', sourceMap: true } } ], }, // images { test: /\.(png|jp(e*)g|svg)$/, use: [{ loader: 'url-loader', options: { limit: 8000, // Convert images < 8kb to base64 strings name: 'images/[hash]-[name].[ext]' } }] }, //File loader used to load fonts { test: /\.(woff|woff2|eot|ttf|otf)$/, use: ['file-loader'] } ] } };
The config is almost the same like webpack.config.js with 2 extra parameters into the ”entry’ point:
– we added ‘webpack/hot/dev-server’ in order to activate HMR, and
– webpack-dev-server/client?http://localhost:8080/ specifying the client domain and port.
Also we added a plugins section and loaded HotModuleReplacementPlugin.
Webpack Dev Middleware
This is the most advanced setup and it uses Express web server with Webpack Dev Middleware. So far we used the builtin Express server from Webpack-dev-server, but now we will use standalone express server. Let’s install it first. And we will need it later not only in development mode so let’s skip ‘–dev’.
yarn add express
yarn add webpack-hot-middleware --dev
create the server file
./server-middleware.js
const express = require('express'); const webpackDevMiddleware = require('webpack-dev-middleware'); const webpackHotMiddleware = require('webpack-hot-middleware'); const webpack = require('webpack'); const webpackConfig = require('./webpack.middleware.config.js'); const app = express(); const path = require('path'); const compiler = webpack(webpackConfig); app.use(webpackDevMiddleware(compiler, { hot: true, publicPath: webpackConfig.output.publicPath, })); app.use(webpackHotMiddleware(compiler, { log: console.log, path: '/__webpack_hmr', heartbeat: 10 * 1000, })); app.get('/', function(req, res) { res.sendFile(path.join(__dirname, 'index.html')); }); const server = app.listen(8080, function() { const host = server.address().address; const port = server.address().port; console.log('App is listening at http://%s:%s', host, port); });
we are using express server with ‘webpack-dev-middleware’,
and the config file
./webpack.middleware.config.js
const webpack = require('webpack'); module.exports = { mode: 'development', entry: [ '@babel/polyfill', './src/index.js', 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000', ], output: { filename: '[name]-bundle.js', publicPath: '/dist', }, plugins: [ new webpack.HotModuleReplacementPlugin(), ], module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: "babel-loader" } }, // SCSS { test: /\.scss$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true, importLoaders: 2, localIdentName: '[folder]-[local]', sourceMap: true } }, { loader: 'postcss-loader', options: { plugins: () => [require('autoprefixer')()], sourceMap: true }, }, { loader: 'sass-loader', options: { outputStyle: 'expanded', sourceMap: true } } ], }, // images { test: /\.(png|jp(e*)g|svg)$/, use: [{ loader: 'url-loader', options: { limit: 8000, // Convert images < 8kb to base64 strings name: 'images/[hash]-[name].[ext]' } }] }, //File loader used to load fonts { test: /\.(woff|woff2|eot|ttf|otf)$/, use: ['file-loader'] } ] } };
Give it a try by executing yarn start-middleware and you will see the same result like in the previous two set ups. Site is running and hot module reloader is enabled.
Running server scripts using latest ES syntax
If you noticed we are using `import … from …` syntax everywhere under ./src folder instead of `const module = require(‘…’)` since we set up Babel to transpile everything under this folder to the latest ES6 syntax.
But if we try to use this in some of the ./server.api.config.js or ./server.middleware.config.js it will fail since there is nothing to transpile these files to the latest ES6 yet. Fortunately there is very straight forward solution. Let’s use Babel-node to run the scripts instead of node. Like it says in the Babel-node website:
“babel-node is a CLI that works exactly the same as the Node.js CLI, with the added benefit of compiling with Babel presets and plugins before running it.”
Perfect for our needs. Now all we need to do in order to have the benefit of EC6 in our server scripts is to replace node
instance with babel-node
and run scripts using babel-node instead.
But first let’s install babel-node
yarn add @babel/node --dev
then edit package.json and replace all instances of node
with babel-node
./package.json
... "scripts": { "start-cli": "webpack-dev-server --hot --history-api-fallback --config webpack.cli.config.js", "start-api": "babel-node server-api.js", "start-middleware": "babel-node server-middleware.js", "clean": "rm -rf ./dist", ...
now it should be perfectly safe to start using latest ES6 into our server scripts. We could go ahead and change all server side scripts to use import ... from
syntax.
Now we have our project set up to use EC6 on the front-end and the back end as well!
Adding base Webpack config.
In the previous chapter we created three different ways of running our project:
- Through Webpack-dev-server Command line interface (CLI) which is using `
yarn start-cli
which uses: webpack.cli.config.js - Through Webpack-dev-server API
yarn start-api
which uses: webpack.api.config.js - Through Webpack-dev-server middleware
yarn start-middleware
which uses: webpack.midleware.config
but if we look at the config files we will notice that most of the parameters are the same. It might make more sense if we create one base config and override only the parameters that are different in these 3 config files. Let’s do this:
– copy the contents of webpack.api.config.js and create a new file called webpack.base.config
./webpack.base.config.js
module.exports = { mode: 'development', entry: [ '@babel/polyfill', './src/index.js', ], output: { filename: '[name]-bundle.js', publicPath: '/dist', }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: "babel-loader" } }, // SCSS { test: /\.scss$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true, importLoaders: 2, localIdentName: '[folder]-[local]', sourceMap: true } }, { loader: 'postcss-loader', options: { plugins: () => [require('autoprefixer')()], sourceMap: true }, }, { loader: 'sass-loader', options: { outputStyle: 'expanded', sourceMap: true } } ], }, // images { test: /\.(png|jp(e*)g|svg)$/, use: [{ loader: 'url-loader', options: { limit: 8000, // Convert images < 8kb to base64 strings name: 'images/[hash]-[name].[ext]' } }] }, //File loader used to load fonts { test: /\.(woff|woff2|eot|ttf|otf)$/, use: ['file-loader'] } ] } };
edit all three config files to look like this:
./webpack.cli.config.js, ./webpack.api.config, ./webpack.midleware.config
let config = require('./webpack.base.config.js'); module.exports = config;
Let’s give it a try and see if HMR still works by edit something like ./src/components/Greetings/index.js for example, and add some extra character inside the H2 tag and observe if the changes will apply
<h2>Hello, {props.user}</h2>
- Starting with CLI
yarn start-cli
looks good - Starting with Dev server API
yarn start-api
looks good but HMR doesn’t work - Starting with Dev middleware
yarn start-midleware
also looks good but HMR is broken as well.
Looks like HMR works only for the CLI setup because we are not adding HotModuleReplacement plugin required for the other two set ups. Let’s fix this:
./webpack.api.config
const webpack = require('webpack'); let config = require('./webpack.base.config.js'); config.entry = [ '@babel/polyfill', './src/index.js', 'webpack/hot/dev-server', 'webpack-dev-server/client?http://localhost:8080/', ]; config.plugins = [ new webpack.HotModuleReplacementPlugin(), ]; module.exports = config;
./webpack.middeware.config
const webpack = require('webpack'); let config = require('./webpack.base.config.js'); config.entry = [ '@babel/polyfill', './src/index.js', 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000', ]; config.plugins = [ new webpack.HotModuleReplacementPlugin(), ]; module.exports = config;
now give it a try and HTR should work just fine.
There is another way to achieve this by using webpack-merge module, but I personally don’t like to add extra module if I could achieve the same result with plain JavaScript.
Adding source map for easy debugging
let’s go to ./src/components/greetings/index.js and intentionally made an error
function Greetings(props) { return(<div className={styles.wrapper}> <h2>Hello, {props.user.test}</h2> </div>);
Run the server and look at the console. You will see something similar as this:
}, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h2", null, "Hello ", props.user.test)));
This doesn’t show you where the error actually is in the original file but it shows the error in the bundle. Let’s fix this by adding ‘source-mapping‘ in our webpack.base.config
module.exports = { mode: 'development', devtool: 'source-map',
Restart the sever and check the console: now the error points to the original file in ./src/components/greetings/index.js
make sure that before we continue you go and remove .test
so the component will not break.