All Posts

Let's Build: HTML5 Development Server & Build Tool With Webpack - Building for Production

Welcome everyone to the last part of this tutorial series. Last time, we were able to create a new page and we also installed a Javascript UI plugin. From how we have progressed since we started this series, we have touched most of the features that front-end developers need to build static pages with the help our webpack based development server and build tool.

As we end this series, I will be sharing to you how to reorganize the Webpack configuration and generate a production build for your static website.

Splitting Webpack Configuration for Development and Production

The first thing that we will be doing is we’ll be making a major change with our Webpack configuration. Currently the Webpack configuration that we have is contained inside webpack.config.js. However, the way our development server loads all the files(HTML, CSS, JS, etc.) is different and is more presented in a flat directory structure which is something like the example below:

├── index.css
├── image.jpg
├── index.js
├── index.html
└── carousel.html

For the build tool that we are building, we are going to generate the production build directory in such a way that we will separate HTML, CSS, Javascript, images and fonts to their own directories. This will require us to split our current Webpack configuration, where we will have a configuration that is separate for the development server and for the production build.

For this tutorial, this is the sample production build directory structure that we’re trying to achieve:

├── css
│   └── fonts
├── img
├── js
├── index.html
└── carousel.html

To start splitting up the Webpack configuration, picking up from the last state of your project directory based from part 5, follow these steps:

1. Install the following npm packages inside your root directory

npm install --save-dev clean-webpack-plugin mini-css-extract-plugin webpack-cli webpack-merge

If you’re wondering what these packages are, you will see what these packages do in the steps that will be coming after this.

2. Create three new files inside the root of the project directory.

cd ./root_of_the_project
touch webpack.dev.config.js
touch webpack.prod.config.js
touch pages.js

Notes:

  • webpack.dev.config.js will contain specific configuration for the development server
  • webpack.prod.config.js will contain specific configuration for the production build
  • pages.js will contain the list of entry points / pages of the website

3. Visit pages.js and paste the following code, containing the entry points / pages of the website:

const pages = [
  {
    entryName: 'index',
    entryScript: './src/pages/index/index.js',
    htmlFile: 'index.html',
    htmlTemplate: 'src/pages/index/index.html',
  },
  {
    entryName: 'carousel',
    entryScript: './src/pages/carousel/carousel.js',
    htmlFile: 'carousel.html',
    htmlTemplate: 'src/pages/carousel/carousel.html',
  }
]

module.exports = pages

This is our custom configuration list of entry points for Webpack to scan through with the corresponding HTML template which html-webpack-plugin will use to generate the output HTML.

4. Open webpack.config.js and replace everything with the code below:

const path = require('path')
const pages = require('./pages')

module.exports = {
	entry: {
    //Instead of hard coding each entry points, we will
    //get all entry points from the pages.js that we created
    ...pages.reduce((prev, next) => {
      return {
        ...prev,
        [next.entryName]: next.entryScript
      }
    }, {})
  },
  module: {
    rules: [
    //The only loader configuration that is shared for 
    //both development and production is resolving javascript files
    //https://github.com/babel/babel-loader
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
        }
      },
    ]
  },
  //Lastly, we'll keep the alias 
  //when making imports from the assets directory
  resolve: {
    alias: {
      Assets: path.resolve(__dirname, "src/assets")
    }
  },
}

5. Open webpack.dev.config.js and paste the code below for the development server:

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { merge } = require('webpack-merge')
const common = require('./webpack.config')
const pages = require('./pages')

// `merge` from `webpack-merge` combines the configuration from
// webpack.config.js with this configuration for the development server
// https://github.com/survivejs/webpack-merge
module.exports = merge(common, {
  mode: 'development',
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: [
          {
            loader: 'file-loader',
          }
        ]
      },
      {
        //This is a regex of file extensions for fonts
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [
          'file-loader',
        ],
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader'
        ]
      },
    ]
  },
  plugins: [
    // Instead of hard coding entries for the HTML
    // pages, we now rely on a single configuration object
    // from pages.js to also include  HTML file name and the HTML
    // template respective to its entry point
    ...pages.map((page) => {
      return new HtmlWebpackPlugin({
        chunks: [page.entryName],
        inject: false,
        filename: page.htmlFile,
        template: page.htmlTemplate,
      })
    })
  ],
  output: {
    filename: '[name].bundle.js',
    publicPath: '/'
  },
})

In the webpack.dev.config.js, the configuration is pretty much the same as how it was before we split the configuration. This time, we just left off some parts to webpack.config.js which will be shared with the production config.

6. Open webpack.prod.config.js and paste the code below for the production build:

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
const { merge } = require('webpack-merge')
const common = require('./webpack.config')
const pages = require('./pages')

module.exports = merge(common, {
  mode: 'production',
  // Remove sourcemaps in production to reduce bundle output size
  devtool: false,
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // MiniCssExtractPlugin, extracts the CSS and creates an output CSS file
          // for the CSS files which are `imported` from JS files
          // https://webpack.js.org/plugins/mini-css-extract-plugin/
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              outputPath: 'img',
              publicPath: (url, resourcePath, context) => {
                // If you're wondering what's this for,
                // I implemented a naming convention that for all images that is meant
                // to be a CSS background, it must have a prefix `cssbg-`. 
                
                // This is a workaround to solve a problem where, given 
                // that the CSS and images resides in a different directory,
                // the `url()` when calling CSS backgrounds by default assumes the
                // background image exists in the same directory as the CSS file.
                
                // The best way to fully understand this is to generate the production build
                // and look at the output CSS to see what it does.
                if(/cssbg-/.test(resourcePath)) {
                  return `../img/${url}`;
                }
                return `img/${url}`;
              },
              options: {
                name: '[name].[ext]',
              },
            }
          }
        ]
      },
      {
        //This is a regex of file extensions for fonts
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              // I decided to put fonts inside the CSS directory since it's
              // exclusively used by CSS anyway.
              outputPath: 'css/fonts',
              publicPath: 'fonts'
            }
          }
        ],
      },
    ]
  },
  plugins: [
    // CleanWebpackPlugin's purpose is to clean up the dist directory
    // every time we generate a production build to make sure
    // we have fresh production files post build
    new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }),
    // Similar to `webpack.dev.config.js, the only difference that 
    // we have in this configuration is `minify: false`. 
    // What it does is it just prevents minifying the HTML output by default.
    // This is useful, specially when you are working with back-end developers
    // who will integrate our production build to their back-end framework's
    // templating engine.
    ...pages.map((page) => {
      return new HtmlWebpackPlugin({
        chunks: [page.entryName],
        inject: false,
        filename: page.htmlFile,
        template: page.htmlTemplate,
        minify: false
      })
    }),
    // MiniCssExtractPlugin, extracts the CSS and creates an output CSS file
    // for the CSS files which are `imported` from JS files
    // https://webpack.js.org/plugins/mini-css-extract-plugin/
    new MiniCssExtractPlugin({
      filename: 'css/[name].css',
      chunkFilename: 'css/[name].css',
    }),
  ],
  optimization: {
    // This is a configuration for extracting imported modules inside an entry point
    splitChunks: {
      cacheGroups: {
        // "commons" cache group intends to extract  imported modules which at least are being imported for at least 2 times
        // https://webpack.js.org/plugins/split-chunks-plugin/#split-chunks-example-1
        commons: {
          name: 'commons',
          chunks: 'initial',
          minChunks: 2
        },
        // "vendors" cache group intends to extract imported npm modules from node_modules
        // https://webpack.js.org/plugins/split-chunks-plugin/#split-chunks-example-2
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  },
  output: {
    // Generate all javascript files inside /dist/js
    filename: "js/[name].bundle.js",
    // Output directory
    path: path.resolve(__dirname, "dist"),
    // The URL root path that will be indicated in <script> and <style> tags
    publicPath: "./"
  },
}) 

Adjustments to Other Files

After splitting the webpack configuration, we have a few files left to adjust to make sure that we get to retain the development server after all the changes that will enable us to generate the production build of the website.

  1. In package.json, add in scripts a build entry, so that we can run webpack-cli to generate the production build of the website.
  "scripts": {
    "start": "node server.js",
    "build": "webpack --config webpack.prod.config.js"
  },
  1. In server.js, replace the config to webpack.dev.config.js
const express = require("express")
const webpack = require("webpack")
const webpackDevMiddleware = require("webpack-dev-middleware")
const app = express()
// Replace `webpack.config.js` with `webpack.dev.config.js`
const config = require("./webpack.dev.config.js")

//the rest of the code here...
  1. Inside /src/assets/images, rename bg.png to cssbg-pattern.png. This is in response to the configuration workaround that I mentioned earlier in in Step 6, in order to reference background images URL properly in the production build.
  2. In global.css, make the necessary adjustments to the background image file being referenced with the cssbg- prefix.
/* ...other CSS code in global.css here */


/* Replace bg.png with cssbg-pattern.png */
body {
  background: #fff url('Assets/images/cssbg-pattern.png') 0 0 repeat;
}

Output

Once you’re done implementing the steps above, the next step for us is to test the development server first if it still works. You can do that by using your terminal inside your project root directory and running:

npm start

If something goes wrong, you may refer to the part 6 source code. If all is good, the next step is for you to generate the production build using webpack. For you to do that, once again on your terminal, inside your project root directory, run:

npm run build

Wait for a few seconds while the build process is running. Once the build process is done, you must see a dist directory containing the production build of your source code.

Image

If that’s what you see, congratulations! You now can start generating a production ready build for your website’s source code. 🎉

Conclusion

This wraps up our series, Let’s Build: HTML5 Development Server & Build Tool With Webpack. With what you’ve accomplished today, you now have a complete development tool to create static websites and web applications where you have a development server that has established a simple system of organizing files per page and for common dependencies. At the same time, it also allows you to use latest the Javascript syntax and the latest ways of developing in CSS with PostCSS. Lastly, enables you to turn your development code into a production-ready code which you may use to build your own website or integrate to the view layer of the back-end framework that you are using. With everything that you have from this tutorial, feel free to customize and extend this code to satisfy your needs as a developer.

With that, I hope that this tutorial series was able to open your eyes with what you can do with tools like Webpack and Bbabel which is commonly used in a much more complex development and build tools like Create React App.

In case you are experiencing any issues by following the instructions in this tutorial please visit this github linkfor the complete source code of this tutorial.

Have a blessed week to you! 🙏


Hi! If you have any questions, suggestions, corrections, or constructive feedback about this post, please let me know. I will greatly appreciate it!💯