HTML Webpack Plugin - create HTML files for webpack generated chunks

How to use webpack and HTML Webpack Plugin to generate HTML pages for multiple entries in 2 steps?

Here’s how to create an HTML for a specific or every entry file using Webpack and HTML Webpack Plugin. A really short explanation.

Webpack Bundling in a Nutshell

Webpack allows you to bundle your app from one or multiple files. It starts from a file called “entry” and follows your import or require calls. It then smartly concatenates your files and even splits them for lazy loading if needed.

Webpack can also accept multiple entries. For each entry, a bundle will be created. When bundling a front end entry point, you sometimes want to generate an HTML file per entry.

If you want to learn webpack from scratch, there’s an old article of mine here that walks you through building a full webpack project.

Real Life Example

Let’s take a look at a real life example from a production setup. You can follow the explanation and run it on your machine locally to better illustrate this use case.

In this setup, we have a UI components library with various components. For each component, we create a test page. This test page is then snapshot-tested using playwright for a visual regression test.

Here’s the repository: https://github.com/Vonage/vivid

The magic happens in the ui-tests folder. If you clone the repository, yarn install and then build the ui-tests (yarn build-ui-tests) webpack will build a ui-tests/tmp folder as well as a ui-tests/dist.

In the tmp folder you’ll find the compiled results of the source code. This is done using a custom webpack plugin that takes the source code and adds it to a test template file.

In dist folder you’ll find multiple html and js files – each for a different component.

If you run yarn ui-tests -s a webpack-dev-server will run and allow you to browse the test cases corresponding to the files in the dist folder.

Reasons to use multiple entries with webpack

Generating multiple HTML outputs for multiple entries can be useful for various reasons. You’d might want to generate multiple applications, or to demo upload a static website to github pages.

In my case, I used it in order to generate dynamic test pages for our visual regression system. I also used the same system to create a development environment for our Design System web components.

This development environment looks like this:

How to generate multiple entries?

The entry property in the Webpack configuration can accept an object with named entries. It can look something like this:

{
  "entry1": "path/to/entry1",
  "entry2": "path/to/entry2"
}

If we have multiple entries, we can even create this object dynamically.

Let’s assume we have multiple components and we can get their names in some way:

const listOfComponents = ['component1', 'component2', 'component3'];

It can be just a manual list of names, as you see in the code above. It can also be something more complex like using nodejs fs in order to get them from the file system:

const listOfComponents = getComponentsFolders().filter(component => !componentsExcludeList.includes(component));

In any event, we have a list of components – the entry names.

We can now generate a list of named entries. We can do this manually, as we saw above. Or we can accomplish it with code that looks something like this:

const entry = listOfComponents.reduce((entries, componentName) => {
	entries[componentName] = path.join(__dirname, `tmp/${componentName}/index.js`);
	return entries;
}, {});

entry looks like this eventually:

{
"vwc-accordion": "/Users/ykra/projects/my-app/ui-tests/tmp/vwc-accordion/index.js",
"vwc-audio": "/Users/ykra/projects/my-app/ui-tests/tmp/vwc-audio/index.js",
"vwc-badge": "/Users/ykra/projects/my-app/ui-tests/tmp/vwc-badge/index.js",
"vwc-banner": "/Users/ykra/projects/my-app/ui-tests/tmp/vwc-banner/index.js",
"vwc-button": "/Users/ykra/projects/my-app/ui-tests/tmp/vwc-button/index.js",
}
An entry object that maps a lot of entries to their relative input file

Each entry has a name (the property name like vwc-accordion) and a path to the actual entry file. We will use the entry’s name when generating the HTML files (the number of entries is virually limitless – in our project we have around 30 entries – one for each component).

Inside our webpack.config.js file we can then use the entry in order to tell webpack to to create my chunks like this:

const config = {
entry,
output: {
filename: "[name].bundle.js", // the file name would be my entry"s name with a ".bundle.js" suffix
path: path.resolve(__dirname, "dist") // put all of the build in a dist folder
}
}
Using the entry in the webpack config and mapping every entry to an output chunk using the output property

Webpack takes the entries and bundles them to their own files according to the output definition: call the bundles according to the entry name and put them all in the dist folder.

How to generate an HTML output for every entry with HTMLWebpackPlugin?

HTMLWebpackPlugin accepts two important arguments for the task at hand.

The first is chunks, which accepts what chunks to add to the HTML file.

The second is filename – which will define the name of the HTML file.

So all we need to do is generate a list of HTMLWebpackPlugin instances with the right chunk names. Now that we understand how to generate multiple entries, we can use the same trick in order to generate that list:

const htmlGenerators = listOfComponents.reduce((entries, componentName) => {
entries.push(new HtmlWebpackPlugin({
inject: true,
chunks: [componentName],
filename: `${componentName}.html`
}));
return entries;
}, []);
For each component we create an HTMLWebpackPlugin instance

In essence, we generate an array of HTMLWebpackPlugin instances.

Each instance states the chunk to be used as an entry name – the same we stated in the entry object for the webpack entry property. This is how HTMLWebpackPlugin “knows” to generate the HTML to the specific entrypoint’s bundle.

The filename can be whatever – but makes sense to use the entry name as well (in this case, the componentName is used for the entry name).

Eventually I wrap it up in the webpack config like this:

const config = {
entry,
output: {
filename: "[name].bundle.js", // the file name would be my entry"s name with a ".bundle.js" suffix
path: path.resolve(__dirname, "dist") // put all of the build in a dist folder
},
plugins: [
new CleanWebpackPlugin(), // use the clean plugin to delete the dist folder before a build
…htmlGenerators
]
};
Adding the htmlGenerators to the plugins list

And this is all that is behind the magic of multiple HTML files for multiple entries with HTMLWebpackPlugin.

Summary

Generating multiple HTML outputs for multiple entries can be useful for various reasons. It is very easy to generate it using Webpack and HTMLWebpackPlugin.

I used it in order to generate dynamic test pages for our visual regression system. I also used the same system to create a development environment for our Design System web components.

Using the same setup, I’ve created a simple “main page” that allows developers to navigate between the different tests and test them live using a webpack dev server.

I add a main entry that will connect the other entries by adding another property to the entry object:

entry.mainPage = (path.join(__dirname, "assets/main.js"));

The main.js file practically creates a menu that redirects to the other HTML files. In essence, we eventually get a “website” for developers to work with on every component in isolation. Using webpack’s dev server allows us a better development experience. For instance, we get HMR out of the box.

Webpack is a great tool. Very versatile with a huge ecosystem. The HTMLWebpackPlugin was there quite from the start – and I believe it is one of the most popular of webpack’s plugins. Now you know how to use it in order to generate multiple HTML files for multiple entries.

Thanks a lot to Michael Perez and Yuval Bar Levi for the review of this article 🙂

Sign up to my newsletter to enjoy more content:

3.8 4 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments