Skip to content

Instantly share code, notes, and snippets.

@mikahimself
Last active April 5, 2021 11:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mikahimself/c27bf56b49ae5578d6bd5ef1c6d0c1b3 to your computer and use it in GitHub Desktop.
Save mikahimself/c27bf56b49ae5578d6bd5ef1c6d0c1b3 to your computer and use it in GitHub Desktop.

webpack related scripts in package.json

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.

piping/composing commands

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.

Debugging

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.

Webpack core concepts

Entry

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

Output tells Webpack WHERE and HOW to distribute the bundles (or compilations) it creates.

module.exports = {
  output: {
    path: './dist',
    filename: './bundle.js'
}

Loaders and rules

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' },
  ]
}

Rules - test

A regular expression that instructs the compiler which files to run the loader against

Rules - use

An array/string/function that returns loader objects

Rules - enforce

Can be either "pre" or "post". Tells Webpack to run this rule before or after all other rules

Rules - include & exlude

Arrays of regular expressions that tell the compiler which folders and files to ignore.

Chaining loaders

Loaders always execute from right to left.

  rules: [
    { test: '/\.less/$', use: 'style-loader, css-loader, less-loader' },
  ]

Plugins

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()
  ]
}

Passing variables to Webpack config

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
  );
}

Handling CSS

Development

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"]
            }
        ]
    }

Production

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"]
        } 
      ]
    },

Common loaders

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.

Presets

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",  

Bundle analyzer

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",

Compression plugin

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

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/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment