Blog » Webpack 4 config explained (with example)

Webpack 4 config explained (with example)

2018-04 |

Ta treść jest dostępna tylko w języku angielskim

Webpack 4 config explained (with example)

Using a skeleton for your application prepared by someone else comes with great benefit of a lot of time saved, but also with huge cost of a lot of knowledge not obtained. Sometimes you'll manage to complete your assignment just fine with some predefined boilerplate, without too much need for deep investigation of it's nooks and crannies. Other times, you'll end up in a position, where you reverse engineer it in order to introduce some major change, or just give up and start from scratch with your own thing. I wouldn't like you to give up on my application skeleton. Thus, I'll describe some of it's shenanigans in it's documentation. Today, I'm explaining build process.

Here's what happens when you type npm run build:base and hit enter on cloned react-redux-typescript-boilerplate repository. It invokes Webpack (with --color parameter, because I like colors), a module bundler, which will build our application and output results in dist directory in project root. By "build" I mean transpilation of TypeScript code to ECMAScript 6 (officially known as ECMAScript 2015), then transpilation from ES6 to browser-compatible JavaScript code with Babel (they actually use word "compiler" in their documentation now), then copying static assets, and a few other steps here and there. Webpack finds it's configuration in it's standard, expected location, which is root directory of our project, in a file named webpack.config.js. Actual configuration, however, is located in /scripts/bundler directory (this is because it's also used in other parts of the project).

const getBundlerConfig = (environment = 'dev', mode = 'normal') => {
  const devEnv = environment === 'dev';

  console.log(`Bundling ${environment} package (mode: ${mode}`);

  return {
    entry: getEntry(),
    output: getOutput(),
    resolve: getResolve(),
    module: getModule(devEnv),
    plugins: getPlugins(devEnv, mode),
    mode: getMode(devEnv),
  }
};

Main function of this script generates Webpack configuration. It will be different, depending on environment, and if it's meant for local server, or if it's meant for audit, which is represented by mode parameter.

The configuration is verbose and not much self-explanatory to say the least. That's the main issue people have with Webpack, from what I hear, and it's also the main reason why this kind of explanation makes sense. And it's already on Webpack 4, believe me, it's been worse.

Entry

const getEntry = () => {
  const main = [
    './src/index.tsx'
  ];

  const vendor =  [
    'axios',
    'babel-polyfill',
    'history',
    'immutable',
    'react',
    'react-dom',
    'react-redux',
    'react-router-dom',
    'react-router-redux',
    'redux',
    'redux-immutable',
    'redux-saga',
  ];

  return {
    main,
    vendor,
  };
};

First item in the configuration, entry, defines entry point for your JS files - where Webpack should start building it's dependency graphs. It is divided into two fields. main represents entry point to package with application source code - starting point of your application. From there, Webpack will basically analyze all imports and include them into this file. As a result, this file will often be a subject for change. vendor, on the other hand, represents all external dependencies, which will be naturally modified much more rarely and thus can be cached for increased performance. Content of this section is more less equal to dependencies section of package.json file.

Output

const getOutput = () => {
  return {
    path: path.resolve('dist'),
    publicPath: '/',
    filename: 'assets/js/[name].[hash].js',
  };
};

Second item, output, tells Webpack where to emit the bundles it created, and how to name them. Therefore, field path pointing to /dist directory. Field publicPath defines a path where all assets that are not imported directly in your code, but linked, will be pointed to. Then, there are names and locations of files that will be produced as a result of this build. Using [name] means, that there will be main and vendor files produces, as defined in entry. Additionally, using [hash], will add a unique string to the file name, which can help with cache busting for both development and production purposes.

Resolve

const getResolve = () => ({
  modules: [
    'src',
    'node_modules'
  ],
  extensions: [
    '.ts',
    '.tsx',
    '.js'
  ]
});

This section defines how modules are resolved. Quoting Webpack documentation, "webpack provides reasonable defaults", and it appears to be true, so we only want to tell it where should he look for our modules and how do they look like.

Module

const getModule = (devEnv) => {
  let typeScriptLoaders = [];

  if (!devEnv) {
    typeScriptLoaders.push('babel-loader');
  }

  typeScriptLoaders.push(
    {
      loader: 'awesome-typescript-loader',
      query: {
        configFileName: 'tsconfig.json',
        silent: true
      }
    },
    {
      loader: 'tslint-loader',
      query: {
        configFile: 'tslintconfig.json'
      },
    }
  );

  const styleLoaders = ExtractTextWebpackPlugin.extract({
    use: [
      {
        loader: 'css-loader',
        options: {
          minimize: !devEnv,
        }
      },
      {
        loader: 'sass-loader',
        options: {
          includePaths: ['./src']
        }
      }
    ],
  });

  const fontLoaders = [{
    loader: 'file-loader',
    options: {
      name: 'assets/fonts/[name].[ext]?[hash]',
    },
  }];

  const imageLoaders = [{
    loader: 'file-loader',
    options: {
      name: 'assets/images/[name].[ext]?[hash]',
    },
  }];

  if (!devEnv) {
    imageLoaders.push('image-webpack-loader');
  }

  return {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        loaders: typeScriptLoaders,
      },
      {
        test: /\.scss$/,
        loader: styleLoaders,
      },
      {
        test: /\.(eot|svg|ttf|woff|woff2)$/,
        loader: fontLoaders,
      }, {
        test: /\.(gif|ico|jpe?g|png|svg)$/,
        loaders: imageLoaders,
      },
    ],
  };
};

module section is where some fun starts, with creative ways to utilize Webpack. This section defines how different types of modules will be treated by Webpack. Different kinds of assets can be imported in your code, and will automatically added to the resulting bundle, if handled properly in this section. Each kind of asset may be treated differently - copied, bundled, modified - with different loader, based on regular expression matching the file name.

Loaders for each type of asset are invoked in a sequence, starting from the last one in the array. That's why for files with TypeScript code there will be TSLint Loader invoked first, which will present it's output in the console (as defined in tslintconfig.json). Then, Awesome Typescript Loader will transpile TypeScript code to ES6 (as defined in tsconfig.json), and then Babel (in case of code meant for production environment) will transpile it further to JavaScript understood by common browsers (as defined in .babelrc).

For styles, they will be first transpiled from SCSS to CSS with SASS Loader, and then bundled with CSS Loader. Moreover, Extract Text Webpack Plugin will move all CSS modules to separate file, as defined in plugins section of this configuration. For other kinds of modules that can be imported in your TS code, they will be treated with File Loader (or Image Webpack Loader, in case of need for production-level image optimization). All of them will then be put in /dist/assets directory.

Plugins

const getPlugins = (devEnv, mode) => {
  let plugins = [
    new webpack.DefinePlugin({
      'process.env':{
        'NODE_ENV': JSON.stringify(devEnv ? 'development' : 'production')
      }
    }),
    new ExtractTextWebpackPlugin({
      filename: 'assets/styles/style.[hash].css',
      allChunks: true,
    }),
    new CopyWebpackPlugin([{ from: './src/assets/static/', to: './assets/'}]),
    new HtmlWebpackPlugin({
      template: './src/index.ejs',
      favicon: './src/assets/favicon.ico',
      title: appConfig.title,
      meta: appConfig.meta,
      inject: false,
      mobile: true,
      unsupportedBrowser: false,
      links: [],
      scripts: [],
      minify: devEnv ? false : {
        collapseBooleanAttributes: true,
        collapseInlineTagWhitespace: true,
        collapseWhitespace: true,
        keepClosingSlash: true,
        minifyCSS: true,
        minifyJS: true,
        minifyURLs: true,
        removeComments: true,
      },
    }),
  ];

  if (!devEnv) {
    plugins.push(
      new webpack.LoaderOptionsPlugin({
        minimize: true,
        debug: false
      }),
      new CompressionWebpackPlugin({
        asset: '[path].gz[query]',
        algorithm: 'gzip',
        test: /\.js$|\.css$|\.html$/,
        threshold: 10240,
        minRatio: 0.8,
      }),
      new ImageminPlugin({
        test: /\.(eot|svg|ttf|woff|woff2)$/,
      }),
    );
  }

  switch(mode) {
    case 'audit':
      plugins.push(
        new BundleAnalyzerPlugin({
          analyzerMode: 'static',
          reportFilename: '../stats/report.html'
        })
      );
      break;

    case 'server':
      plugins.push(
        new webpack.NamedModulesPlugin(),
        new webpack.HotModuleReplacementPlugin()
      );
      break;

    default:
      break;
  }

  return plugins;
};

In plugins section one can do some really nasty magic with Webpack. There is quite big library of Webpack built-in plugins available, and there are much more provided by there community out there. One can always write his own too, using Webpack Plugins API. In this example, I'm using Webpack plugins to copy static assets to dist directory, generate index.html file according to my configuration and automatically include all styles and scripts in it, compress some of my static assets to gzip, generate report about content of my bundled package, enable Hot Module Replacement, and others.

Mode

const getMode = (devEnv) => devEnv ? 'development' : 'production';

The last bit of Webpack configuration is perhaps the most mysterious. Setting this option properly tells Webpack to use its built-in optimizations accordingly, which includes minification and uglification of bundle for production environment.

Finally, as a result of main function of Webpack bundling script, there is a Webpack configuration produced for given build, based on it's environment and mode. Then, the config is applied by Webpack, which bundles the application in certain way, plus performs all those extra tasks like transpilation, compression, copying files in a meantime. Then, operation outcome is printed in console, while resulting bundle lands in dist directory.

I hope that one, having this detailed description of the whole process, will have an easier time modifying it in order to adjust it to his needs.

Inne wpisy

Moving from Sass to Styled Components (with snapshot tests)

2018-08 |

Ta treść jest dostępna tylko w języku angielskim

Moving from Sass to Styled Components (with snapshot tests)

Who doesn't like to constantly rework perfectly fine stuff into something new and fancy just because it's trendy now? Well, probably pretty much every single JS developer, most certainly everyone who works with this language long enough to experience at least a glimpse of famous "JS fatigue" feeling (so approximately a few weeks). However, I created this blog as a playground to try out new libraries and frameworks, and since I'm learning Styled Components for my professional work at the moment, even though Sass-based styling worked perfectly for my needs, I reimplemented all of it into this controversial CSS-in-JS. And I'm still sane!

Czytaj dalej

Mocking and proxying HTTP requests from localhost with Webpack Dev Server or Express

2018-06 |

Ta treść jest dostępna tylko w języku angielskim

Mocking and proxying HTTP requests from localhost with Webpack Dev Server or Express

Back in the day, my JS projects were small and self-contained. Nowadays, in my professional work, majority, if not all of front-end applications I am working on are connected to multiple back-end services for variety of reasons. It gives me a freedom of not caring of the back-end, as long as we have defined a contract, and proceeding happily with what actually matters, i.e. colors and animations. But it also gave ma some headache, when I wanted to have pleasant, convenient coding environment on my localhost, and not have all of these weird back-end stuff running on my local machine as well. Here's how I worked it out with both Webpack Dev Server and Express.

Czytaj dalej

Syntax highlighting in React with highlight.js and Web Worker

2018-09 |

Ta treść jest dostępna tylko w języku angielskim

Syntax highlighting in React with highlight.js and Web Worker

One of the most common issues I heard people have with my blog was lack of syntax highlighting in posts, especially those, which contain a lot of code. Okay, it's almost 2019, I'm a software engineer, working mostly with front-end these times - I finally agreed, that it should be added. So I added it. And in the meantime, I also learned a little bit about Web Workers. Hence, this post, in which I describe this little adventure.

Czytaj dalej

How to integrate Disqus comments in React app

2017-09 |

Ta treść jest dostępna tylko w języku angielskim

How to integrate Disqus comments in React app

One of the challenges that I faced while programming this, so far, very simple blog app (the one that you are using to read this post, most likely), was how to give my readers a possibility to comment on my posts. Obviously, it is one of the crucial features of this kind of app – I would like my readers to be able to tell me where am I wrong, and I would like me to be able to respond to such heretic claims. However, the whole feature seems quite a lot of work to implement from scratch. Luckily, there are plenty of ready out of the box solutions out there, one of them being Disqus.

Czytaj dalej

How to set up tooling around your open source project

2019-11 |

Ta treść jest dostępna tylko w języku angielskim

How to set up tooling around your open source project

Lately in my project we had very particular need for specific validation of content of JavaScript bundle produced by Webpack. In the process of extensive research we have not found any existing tool to serve this purpose. Therefore, I've created Webpack Bundle Content Validator, which is a Webpack plugin and a CLI tool for Webpack bundle content validation. More importantly, I made it open source. Surprisingly, simply publishing it to NPM wasn't enough - I decided to set up continuous integration, automated unit tests code coverage calculation, automated check for vulnerabilities and a few other things, in order to make it legitimate and reliable package. In this post I'm describing those steps in details.

Czytaj dalej