So, why are there a bunch of commands available to use without paths within package.json? Well, because npm creates inside node_modules a folder called .bin that stores a whole bunch of binary executables. These are then available within the scope of package.json.
You can easily pipe commands in package.json:
"webpack": "webpack"
"dev": "npm run webpack -- --mode development"
Now, when you run the dev script, it actually runs the webpack script and slaps "--mode development" at the end.
Node lets you debug things (in this example, webpack.js" with the help of scripts and, for example, Chrome.
"debug": "node --inspect --inspect-brk ./node_modules/webpack/bin/webpack.js",
After running the script, head over to Chrome and type chrome://inspect
in the address bar. The page that opens lets you access devtools for Node. In the edit tools, CTRL+P gives you access to a handy file picker in addition to the possibility of adding breakpoints.
Entry tells Webpack WHAT (i.e. files) to load for the browser. In practice, entry point is the first Javascript file that needs to be loaded to start an application, and this file serves as the starting point for Webpack. We define this file using the "entry" property in the configuration.
module.exports = {
entry: './src/index.js',
}
Webpack looks into this file, picks up the imports from there, looks in those files, picks imports from those and creates a graph of all the dependencies.
Output tells Webpack WHERE and HOW to distribute the bundles (or compilations) it creates.
module.exports = {
output: {
path: './dist',
filename: './bundle.js'
}
Tell Webpack HOW to interpret and translate files, on a per-file basis, before they are added to the dependency graph. Loaders are JavaScript modules (funtions) that take in source files and return them in a modified state.
module: {
rules: [
{ test: '/\.ts/$', use: 'ts-loader' },
{ test: '/\.js/$', use: 'babel-loader' },
{ test: '/\.css/$', use: 'css-loader' },
]
}
A regular expression that instructs the compiler which files to run the loader against
An array/string/function that returns loader objects
Can be either "pre" or "post". Tells Webpack to run this rule before or after all other rules
Arrays of regular expressions that tell the compiler which folders and files to ignore.
Loaders always execute from right to left.
rules: [
{ test: '/\.less/$', use: 'style-loader, css-loader, less-loader' },
]
A plugin is an ES5 class that implements an apply function. Plugins hook into and handle events from the compiler. Plugins can be instantiated so they're first required in the configuration and then instantiated in the plugins section
var BellOnBundlerErrorPlugin = require('bell-on-error');
var webpack = require('webpack');
module.exports: {
// ...
plugins: [
new BellOnBundlerErrorPlugin(),
new webpack.optimize.UglifyJSPlugin()
]
}
You can pass variables to your Webpack config by using the --env
environment variable in package.json. For example.
"dev": "npm run webpack -- --env.mode development --watch",
You can then pick up this variable in webpack.config.js
by converting module.exports
to a function:
module.exports = ({env}) => {
return {
mode: env.mode,
output: {
filename: "bundle.js"
}
};
}
Using the env variable, we can easily create separate configurations for development and production that only contain the differing parts for both, while keeping the common configuration options in the main configuration file. Below, modeConfig
is used to pick up the correct configuration file based on the env, and webpack-merge is used to merge the custom config with the main config.
const webpackMerge = require('webpack-merge');
const modeConfig = (env) => require(`./build-utils/webpack.${env}.js`)(env);
module.exports = ({mode, presets} = { mode: "production", presets: [] }) => {
return webpackMerge(
{
mode,
module: {
rules: [
// ...
]
},
output: {
filename: "bundle.js"
},
plugins: [
new HtmlWebpackPlugin(),
new webpack.ProgressPlugin()
]
},
modeConfig(mode), // Here's where we merge the custom config with the main one
);
}
In development, you can import your CSS in the entry file:
import "./my-css.css":
And then, in your development config, add css-loader to parse the css into an array and style-loader to pass the array into a script tag. Remember that the items in use
are parsed from right to left.
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
}
]
}
For production use, it is better to have the CSS in another tag apart from the script tag so as to not block the main thread with it. To do this, we can use the MiniCssExtractPlugin
.
First, require the plugin at the top of the production configuration:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
Then, add the plugin and possible configuration options to the plugins
array. The default options are often OK, though:
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
})
]
Last, add a rule for handling css modules into the production config. Basically, css files are loaded using css-loader
and then passed onto MiniCssExtractPlugin
and its loader.
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
}
]
},
url-loader
is a tool that you can use to handle audio, video, or image files such as JPEGs on the base configuration level. As a simple example, we can add jpeg handling:
module: {
rules: [
{
test: /\.jpe?g$/,
use: ["url-loader"]
}
]
},
Now, if we have an image that we want to use in our project, we can technically import it in a JavaScript file. This is because loaders enable Webpack to treat everything like it is JavaScript. So, we can do this, for example, to get the base64 encoded version printed into the console.
import image from "./webpack-logo.jpg";
// ...
console.log(image);
Sometimes, especially if the image is very large, it is better to have it copied into the dist
folder instead. You can control the size of the files which get added as base64 and which get copied by setting the limit option for url-loader:
rules: [
{
test: /\.jpe?g$/,
use: [
{
loader: "url-loader",
options: {
limit: 5000
}
}
]
}
]
Behind the scenes, url-loader uses file-loader to copy the image to the dist
directory and to return its url in the dist
directory.
When adding new features to your project, you might not want to add them directly to the main configuration files so as to not break things, or to avoid the hassle of removing them later on, if they do not pan out. Instead, you could create presets to add ad hoc features to your project that you can easily remove later on.
First thing you need is loadPresets.js
script in the build-utils
folder. This script reads the env
passed from the main configuration file, picks up the presets from there, picks up any presets and uses require() to load the preset objects, merge them, and return them to the main configuration:
const webpackMerge = require("webpack-merge")
module.exports = env => {
const { presets } = env; // Pull the presets option from env passed from the main config
/** @type {string[]} */
const mergedPresets = [].concat(...[presets]); // Flatten the presets into an array of strings
const mergedConfigs = mergedPresets.map(
presetName => require(`./presets/webpack.${presetName}`)(env) // Call each preset and pass the env to them
);
return webpackMerge({}, ...mergedConfigs); // Merge the preset configs and return them.
};
Next, we'll need to add loadPresets
to our main configuration:
const presetConfig = require("./build-utils/loadPresets");
module.exports = ({mode, presets} = { mode: "production", presets: [] }) => {
return webpackMerge(
{
// ...
},
modeConfig(mode),
presetConfig({ mode, presets })
);
}
After we have the loadPresets.js
script ready, we can create a sample preset that defines how to handle typescript files. We'll call this file webpack.typescript.js
and place it in ./build-utils/presets
:
module.exports = (env) => ({
module: {
rules: [
{
test: /\.ts$/,
use: "ts-loader"
}
]
}
});
To use this preset, we can simply add a script with a parameter to our package.json
:
"prod": "webpack --env.mode production",
"prod:typescript": "npm run prod -- --env.presets typescript",
One plugin that you might only want to use occasionally, and therefore only use through a preset is the webpack-bundle-analyzer
. This tool helps you visualize the size of webpack output files with an interactive zoomable treemap.
So, let's add a preset that enables us to use the bundle analyzer plugin:
const WebpackBundleAnalyzer = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = (env) => ({
plugins: [
new WebpackBundleAnalyzer()
]
});
And once that's done, let's add a script to package.json
to run it:
"prod:analyze": "npm run prod -- --env.presets analyze",
The compression-webpack-plugin
lets you prepare compressed versions of assets to serve them with Content Encoding. Again, let's create a preset for this:
const CompressionWebpackPlugin = require("compression-webpack-plugin");
module.exports = (env) => ({
plugins: [
new CompressionWebpackPlugin()
]
});
And add a script in package.json
:
"prod:compress": "npm run prod -- --env.presets compress",
And if we wanted to combine analyzing and compressing, we could run the following command on the command prompt:
npm run prod:compress -- --env.presets analyze
Source maps come in many shapes and sizes, each with their own benefits and tradeoffs with regards to source map quality and build time. The creation of source maps is controlled by the devtool
option.
The source-map
option results in the slowest build times, but results in high quality source maps. You can easily access source code with accurate line markings and you can add breakpoints in the Sources tab in Chrome dev tools.
The cheap-module-source-map
is the default option used by Create React App, and builds somewhat faster. It gives mostly good line markings, but is somewhat less readable than the previous option
For detailed infomation about all available option values, see https://webpack.js.org/configuration/devtool/