Setting Up Your Build Kitchen
Welcome to Webpack configuration! If Webpack is like a professional kitchen, then the webpack.config.js file is your recipe book. Today, we'll learn how to write this recipe book from scratch, understanding each ingredient and cooking instruction along the way.
Project Setup
Before we create our configuration, let's set up a new project. Think of this as preparing your kitchen before cooking:
# Create a new project directory
mkdir webpack-setup-demo
cd webpack-setup-demo
# Initialize npm project
npm init -y
# Install webpack and webpack-cli
npm install --save-dev webpack webpack-cli
# Create project structure
mkdir src
mkdir dist
touch src/index.js
touch webpack.config.js
Your project structure should look like this:
webpack-setup-demo/
├── dist/
├── src/
│ └── index.js
├── package.json
└── webpack.config.js
Basic Configuration Structure
Let's start with the simplest possible configuration. This is like learning to boil water before cooking complex dishes:
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
};
Let's break down each part:
- path: Node.js module for handling file paths
- entry: Where Webpack starts building the dependency graph
- output: Where and how to save the bundled files
- path.resolve: Creates an absolute path (required for output.path)
Understanding Entry Points
The entry point is like the main ingredient in your recipe. Webpack starts here and follows all imports to build your application bundle.
Single Entry Point
// Simple entry
module.exports = {
entry: './src/index.js'
};
// Equivalent to:
module.exports = {
entry: {
main: './src/index.js'
}
};
Multiple Entry Points
// Multiple entries for different pages
module.exports = {
entry: {
home: './src/home.js',
about: './src/about.js',
contact: './src/contact.js'
}
};
Output Configuration
The output configuration tells Webpack how to write the compiled files to disk. It's like deciding how to plate and serve your finished dish.
module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js'
},
output: {
filename: '[name].bundle.js', // Uses entry key name
path: path.resolve(__dirname, 'dist'),
clean: true, // Clean the output directory before emit
publicPath: '/assets/', // Public URL of output directory
}
};
Filename Templates
| Template | Description | Example Output |
|---|---|---|
| [name] | Entry point name | app.bundle.js |
| [contenthash] | Hash of file content | app.8e0d62a82.js |
| [fullhash] | Hash of build | app.50b8a3e1c.js |
| [id] | Module identifier | 0.bundle.js |
Mode Configuration
Webpack has three modes that enable different built-in optimizations. It's like having preset cooking modes on your oven:
// Development mode
module.exports = {
mode: 'development',
// Enables:
// - Readable output
// - Fast incremental compilation
// - Helpful error messages
};
// Production mode
module.exports = {
mode: 'production',
// Enables:
// - Minification
// - Scope hoisting
// - Tree shaking
// - Side-effect flagging
};
// None mode
module.exports = {
mode: 'none',
// Disables all optimizations
// Used for debugging webpack itself
};
Module Rules (Loaders)
Module rules tell Webpack how to process different types of files. Think of loaders as specialized kitchen appliances for different ingredients:
module.exports = {
module: {
rules: [
// CSS files
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
// Images
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
// Fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
// CSV/TSV files
{
test: /\.(csv|tsv)$/i,
use: ['csv-loader'],
},
// XML files
{
test: /\.xml$/i,
use: ['xml-loader'],
},
],
},
};
Anatomy of a Rule
Resolve Configuration
The resolve configuration helps Webpack find modules. It's like telling your sous chef where to find ingredients:
module.exports = {
resolve: {
// File extensions to try when importing without extension
extensions: ['.js', '.jsx', '.json', '.css'],
// Directory names to look for when resolving modules
modules: ['node_modules', 'src'],
// Aliases for import paths
alias: {
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
'@styles': path.resolve(__dirname, 'src/styles'),
},
},
};
// Now you can import like this:
// import Button from '@components/Button';
// instead of:
// import Button from '../../../components/Button';
DevServer Configuration
The dev server provides a development environment with live reloading. It's like having a test kitchen where you can taste as you cook:
module.exports = {
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
compress: true,
port: 9000,
hot: true,
open: true,
historyApiFallback: true,
// Proxy API requests
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: { '^/api': '' },
},
},
},
};
DevServer Features
- static: Serve static files from directory
- compress: Enable gzip compression
- hot: Enable Hot Module Replacement
- open: Open browser after server starts
- historyApiFallback: For single-page apps
- proxy: Proxy API requests to avoid CORS issues
Environment Variables
Use environment variables to create different configurations for different environments:
// webpack.config.js
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
entry: './src/index.js',
output: {
filename: isProduction ? '[name].[contenthash].js' : '[name].js',
path: path.resolve(__dirname, 'dist'),
},
devtool: isProduction ? 'source-map' : 'eval-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env.API_URL': JSON.stringify(
isProduction ? 'https://api.production.com' : 'http://localhost:3000'
),
}),
],
};
};
Complete Example Configuration
Let's put it all together with a real-world configuration:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
entry: {
main: './src/index.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction
? '[name].[contenthash].js'
: '[name].js',
clean: true,
},
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
{
test: /\.css$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
}),
isProduction && new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
].filter(Boolean),
resolve: {
extensions: ['.js', '.jsx'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
devServer: {
static: './dist',
hot: true,
open: true,
port: 3000,
},
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
};
Configuration Best Practices
1. Split Configurations
// webpack.common.js
module.exports = {
entry: './src/index.js',
// Common configuration
};
// webpack.dev.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-source-map',
});
// webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map',
});
2. Use Environment Variables
// .env file
API_URL=https://api.example.com
DEBUG=true
// webpack.config.js
const dotenv = require('dotenv');
dotenv.config();
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.API_URL': JSON.stringify(process.env.API_URL),
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
}),
],
};
3. Keep It Organized
- Group related configurations together
- Use comments to explain complex configurations
- Extract repeated values into constants
- Use descriptive names for entry points and chunks
Debugging Your Configuration
When things go wrong, here are some debugging techniques:
// Add to webpack.config.js for debugging
module.exports = {
// ... other config
stats: {
// Display bailout reasons
optimizationBailout: true,
// Display module reasons
reasons: true,
// Display chunk relations
chunkRelations: true,
},
// Use verbose output
infrastructureLogging: {
level: 'verbose',
},
};
// Run webpack with debugging flags
webpack --config webpack.config.js --profile --json > stats.json
// Analyze the stats file
webpack-bundle-analyzer stats.json
Common Pitfalls
1. Incorrect Path Resolution
// WRONG: Relative path in output
output: {
path: './dist' // This will fail
}
// CORRECT: Absolute path
output: {
path: path.resolve(__dirname, 'dist')
}
2. Missing File Extensions
// Add extensions to resolve
resolve: {
extensions: ['.js', '.jsx', '.json']
}
3. Loader Order
// Loaders execute from right to left
use: ['style-loader', 'css-loader', 'sass-loader']
// sass-loader → css-loader → style-loader
Practical Exercise
Create a Webpack configuration for a React application with the following requirements:
- Support for JSX files
- CSS modules with Sass preprocessing
- Image optimization
- Development server with HMR
- Production optimizations
Try to implement this configuration yourself, then check the solution below:
Click to see solution
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
entry: './src/index.jsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction ? '[name].[contenthash].js' : '[name].js',
clean: true,
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
{
test: /\.module\.s[ac]ss$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
},
},
'sass-loader',
],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024, // 8kb
},
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
isProduction && new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
].filter(Boolean),
resolve: {
extensions: ['.js', '.jsx'],
},
devServer: {
hot: true,
port: 3000,
},
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
};
Summary
Today we learned:
- How to structure a webpack.config.js file
- Essential configuration options (entry, output, mode)
- How to configure loaders and rules
- Development server setup
- Environment-specific configurations
- Best practices and common pitfalls
Remember: A good Webpack configuration is like a well-organized kitchen - everything has its place, and the workflow is smooth and efficient!