Initial setup
These days webpack has a pretty decent documentation, yet configuring webpack based application from scratch comes with its challenges. This guide is intended to walk the reader through all the steps of setting up modern webpack based configuration. The intention is to break down the process into major milestones that could be used for future extensions by themselves or coupled together as one basic setup.
npm
package manager is used for the reset of the guide. There is nothing special about the choice andyarn
could be used instead
Let's start installing everything needed for the initial setup. An assumption is made that a default installation folder has been selected and initialized with a default package.json
file.
npm i -y
Dependencies
Webpack
npm i webpack webpack-cli webpack-dev-server -D --save-exact
The core of the webpack setup consist of 3 packages:
webpack
- the bundler itselfwebpack-cli
- webpack command line interface (different options exist)webpack-dev-server
- development server
TypeScript
Webpack configuration is based on a JavaScrip file. This is the point where things start to get complicated. Without knowing different config options and the theory behind them, it takes some patience to settle different parts down. For that reason, we'll be creating our configuration in TypeScript. This will give us both the IDE support and the config file documentation at hand. Following packages are in use:
typescript
- TypeScript compiler itself (including cli)ts-node
- TypeScript execution and REPL provider (so we can transpile and execute ts files in one go)
A bunch of TypeScript type definition packages to support smooth compilation and documentation experience:
@types/node
- type definitions for NodeJs@types/webpack
- webpack type definitions@types/webpack-dev-server
- webpack-dev-server type definitions
npm i @types/node @types/webpack @types/webpack-dev-server -D --save-exact
HTML
Out of the box, webpack understands only a basic dialect of JavaScript. That is a good start, but for the working example an index.html
file generation mechanism is needed. The way to extend webpack capabilities is via plugins. Let's install HTML related plugins and corresponding type definition files:
html-webpack-plugin
- generatesindex.html
file from the given templatehtml-webpack-harddisk-plugin
- makes sureindex.html
file is written to disk (more on this later)@types/html-webpack-plugin
- html-webpack-plugin type definitions
npm i html-webpack-plugin html-webpack-harddisk-plugin @types/html-webpack-plugin -D --save-exact
Utilities
To finish the installation, let's add some utility packages.
clean-webpack-plugin
- webpack plugin for cleaning up generated assetscross-env
- cross platform environment variable setup
npm i clean-webpack-plugin cross-env -D --save-exact
One line setup
npm i @types/clean-webpack-plugin @types/html-webpack-plugin \
@types/node @types/webpack @types/webpack-dev-server \
clean-webpack-plugin cross-env html-webpack-harddisk-plugin \
html-webpack-plugin ts-node typescript webpack \
webpack-cli webpack-dev-server -D --save-exact
TypeScript
We are going to keep all the webpack related configurations in a separate folder. Following commands create webpack
folder, TypeScript config file with default options, and move it to the newly created folder.
mkdir webpack
npx tsc --init --module commonjs --target es5 --strict true --esModuleInterop true --noEmit true
mv tsconfig.json webpack/tsconfig.json
Typescript options:
--module
- we are executing webpack config under Node, hencecommonjs
as a module option--target
- target JavaScript dialect as Ecma 5. This is mostly to make sure webpack runs smoothly. We are not going to generate JavaScript anyway, so the specific target does not represent a major difference.--strict
- strict type checking is enabled--esModuleInterop
- enable default imports (interoperability betweencommonjs
andes6
modules)--noEmit
- do not generate output files
Environments
Out of the box, webpack supports several operational modes, development
, production
, none
. These modes will loosely correspond to a target build environments final setup is going to need. However, having explicitly set environments in the code helps splitting settings for different plugins and presets across different technologies.
environments.js
// @ts-check
const values = Object.freeze({
Prod: 'production',
Development: 'development'
})
/**
* @param {string=} env
*/
module.exports = (env) => {
env = env || process.env.NODE_ENV
return {
get isProduction() {
return env === values.Prod
},
get isDevelopment() {
return env === values.Development
},
get current() {
return env
}
}
}
This time we went with the JavaScript option. That is because not every single part of tech in our stack understands TypeScript (as we'll see later).
Configuration file
Parts
We are going to have two configuration files, one per each environment specified in a previous section. To remove duplications across different webpack config files, we are going to put common parts into parts.ts
inside the webpack folder. Webpack type definitions will help us here making sure we create everything according the specification.
parts.ts
import path from 'path';
import webpack, { Entry, Output, Node, Plugin } from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import HtmlWebpackHarddiskPlugin from 'html-webpack-harddisk-plugin';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
export const distFolder = () => path.resolve(__dirname, '../dist')
export const getParts = () => ({
context: path.join(__dirname, '../src', 'app'),
entry: {
main: './index',
} as Entry,
output: {
path: path.resolve(distFolder(), 'js'),
filename: '[name].bundle.js',
publicPath: '/js'
} as Output,
node: {
fs: 'empty'
} as Node,
plugins: [
new webpack.EnvironmentPlugin(['NODE_ENV']),
new HtmlWebpackPlugin({
chunksSortMode: 'dependency',
filename: '../index.html',
alwaysWriteToDisk: true
}),
new HtmlWebpackHarddiskPlugin(),
new CleanWebpackPlugin({ verbose: true })
] as Plugin[]
})
Here we define a context
of the project. It represents a base/root folder path. Everything else will be resolved relatively.
Next, we define the application main entry
point. The key main
will be used by the webpack to create JavaScript output bundle file.
Next is the output
section. It defines how and where assets are layed out:
path
- represents physical folder path for the assets (JavaScript file in this case)filename
- defines a chunk file name inside the assets folder.[name].bundle.js
means take a chunk name from the entry section and substitute the[name]
with it. So the final output JavaScript file name will bemain.bundle.js
publicPath
- defines how final html page will be including JavaScript asset entries. In our case it will be using/js
prefix and assets file name right after. Example:/js/main.bundle.js
.
node
section defines NodeJs substitutes for the modules not relevant to the web execution engine. For now we have specified only fs
in case some third party library uses it.
plugins
section is used to extend webpack capabilities. In our case, we use:
EnvironmentPlugin
to mapNODE_ENV
key passed from the environment context into the execution environment.HtmlWebpackPlugin
to createindex.html
file automatically with all the assets hooked up.HtmlWebpackHarddiskPlugin
to make sureindex.html
file is written to disk even in dev mode, so webpack dev server can serve it properly.CleanWebpackPlugin
to clean up all the assets generated during previous build execution.
Development
webpack.config.dev.ts
import { Configuration } from 'webpack';
import Environments from './environments.js'
import { getParts, distFolder } from './parts'
const environments = Environments()
console.log(`Running webpack config for environment: ${environments.current}`);
const parts = getParts()
const config: Configuration = {
context: parts.context,
mode: 'development',
entry: parts.entry,
devtool: 'cheap-module-eval-source-map',
output: parts.output,
node: parts.node,
plugins: parts.plugins,
devServer: {
contentBase: distFolder(),
overlay: true,
hot: true,
open: true
}
}
export default config
The major parts here are:
mode
- defines webpack operation mode.devtool
- defines source mapping generation approach.cheap-module-eval-source-map
gives a middle ground between the speed and information provided. But you can experiment with the different options for your set up.devServer
- defines webpack dev server configuration section.contentBase
- defines the folder path to server all the content from (including static assets)overlay
- if there is a compilation error, show full overlayhot
- enable hot module reloadingopen
- opens default browser on start
Production
webpack.config.ts
import webpack, { Configuration } from 'webpack';
import Environments from './environments.js'
import { getParts } from './parts'
const environments = Environments()
console.log(`Running webpack config for environment: ${environments.current}`);
const parts = getParts()
const config: Configuration = {
context: parts.context,
mode: 'production',
entry: parts.entry,
output: parts.output,
node: parts.node,
plugins: [
...parts.plugins,
new webpack.SourceMapDevToolPlugin({
filename: '[name].js.map',
lineToLine: true
})
],
optimization: {
minimize: true,
removeAvailableModules: true
}
}
export default config
This time, the mode is production
and the devtool
option is substituted with the SourceMapDevToolPlugin
.
filename
- file name pattern for source map files.lineToLine
- matched modules are mapped one to one to sources (speeds up mapping generation)
optimization
section defines two additional parameters:
minimize
- minimizes JavaScript assets using uglify-js (default minifier)removeAvailableModules
- do not include modules present in parent chunks
Scripts
Add following scripts to scripts
section under the package.json
file:
build:dev
- builds output assets using development
environment settings. Suitable for inspecting built assets in a dev mode.
"build:dev": "cross-env TS_NODE_PROJECT=\"./webpack/tsconfig.json\" NODE_ENV=development webpack --config ./webpack/webpack.config.dev.ts"
start:dev
- starts the dev server and builds in memory assets. Mostly used for dev work loads
"start:dev": "cross-env TS_NODE_PROJECT=\"./webpack/tsconfig.json\" NODE_ENV=development webpack-dev-server --config ./webpack/webpack.config.dev.ts"
build:prod
- build output assets using production
environment settings. Mostly used for production deployments.
"build:prod": "cross-env TS_NODE_PROJECT=\"./webpack/tsconfig.json\" NODE_ENV=production webpack --config ./webpack/webpack.config.ts",
ts-node
is the execution engine behind webpack configuration using TypeScript. When we execute a script command, at first, the config file will be picked up by the ts-node and transpiled to JavaScript. But for this to happen we need to explain to ts-node where tsconfig.json
file is. Hence we are executing script commands with the additional twist of specifying environment variable for the ts-node project (TS_NODE_PROJECT
).
Sources
Finally, after finishing with all the plumbing, we create the application source code. Create src
folder under the root of the project. Create app
folder under src
folder. Add index.js
file to the app
folder:
mkdir src
cd src
mkdir app
cd app
touch index.js
if (module.hot) {
module.hot.accept()
}
const func = () => {
console.log('test')
}
func()
module.hot.accept
makes sure that we accept and update the module during hot replacement. The method is provided by theHotModuleReplacementPlugin
that ships with the webpack installation.
Try running the app in a dev mode by executing start:dev
command script. If everything is set up correctly, you should see test
written to the browser console.