How to use custom webpack configuration in an Nx project?

Customizing webpack for your own needs is a powerful functionality. It allows you full control of your app’s development and deployment process. In this article we will go over how to do that in an NX monorepo.


TL;DR.


Webpack has been the industry standard bundler for a long time. Today you can do much more than just bundle with it. Nrwl’s NX is a monorepo management framework that sprout from the Angular CLI. It has long since became much more. You can manage all your frontend and backend projects – regardless of framework and soon even regardless of programming language. In this article, I’ll go over a less known part of Nx that was useful for me a few times in the past – customizing the webpack configuration for projects in the monorepo.

Let’s look at a project with a web app. This web app uses web components. Here’s the repository:

https://github.com/YonatanKra/nx-custom-webpack

It doesn’t really matter what the app is doing (it’s just the default app set by Nx for web components). The point is in the main file that looks like this:

import './app.element.css';
export class AppElement extends HTMLElement {
public static observedAttributes = [];
connectedCallback() {
const title = 'custom-webpack';
this.innerHTML = `
<header class="flex">
<h1>Welcome to ${title}!</h1>
</header>
<main>
<h2>Welcome to our app!</h2>
</main>
`;
}
}
customElements.define('custom-webpack-root', AppElement);
view raw app.element.ts hosted with ❤ by GitHub
Code snippet 1: The application’s main element file

A custom element is indeed created in app.element.ts. There are two issues here though.

Problem #1: File Structure

The first is that we hard code the HTML inside the ts file. The HTML might be more complex so we’d might want to separate the HTML from the TS file and create an app.element.html file.

I know this issue is eligible for debate, but let’s assume we’ve decided we want separate HTML and TS files for the sake of this tutorial.

Trying to do this naively gives us this error while trying to build our app:

Figure 1: Webpack failed to bundle our app because it does not know how to handle an HTML file

The solution is written in the error. We need to find a webpack loader that allows us to load HTML files the way we want to. In our case, we’d just like to get the contents of the HTML file as a string. For this, we have the raw-loader.

We first install the raw-loader :

yarn add -D raw-loader

And then we need to add a rule in webpack config to use this loader. Can you spot the webpack config file in the project? Spoiler – there is no such a file.

So how does one configures the webpack config with extra stuff?

How to customize webpack config for an NX project?

  1. Go to the workspace.json file
  2. Find the project you wish to edit
  3. In the project’s targets.build.option add a webpackConfig property with a path to a config file like this:
Code snippet 2: workspace.json part for the custom-webpack application. Lines 28 and 59 designate the location of the custom webpack configuration.
  1. Do the same for the targets.serve.options.
  2. Now create the config file in the designated path.
  3. export a function that receives a config object and a context and returns a modified config like so:
    module.exports = (config, context) => {
    return {
    ...config,
    module: {
    ...config.module,
    rules: [
    ...config.module.rules,
    {
    test: /\.html$/i,
    use: 'raw-loader',
    },
    ],
    },
    };
    };
    view raw webpackConfig.js hosted with ❤ by GitHub

    As shown in the code, the exported function receives the config as an input. It then modifies its module.rule property by adding the raw-loader rule. Notice that we spread the properties we modify in order for them to be in the output and not completely overwritten.
  4. Now you can happily compile your project with the new webpack config!

Problem #2: Shadow DOM CSS Encapsulation

The second issue is how we import the CSS. It is imported using webpack’s css-loader. The css-loader just takes the CSS in the file and adds it to the page. This works if we are using a simple custom element. But one of the powers of web components is the Shadow DOM, which enables CSS encapsulation.

Converting our custom element to a web component using Shadow DOM, will result in a styleless component:

Figure 2: The app after encapsulation (top) vs. before encapsulation

Of course we’d like our app to have the style. For this, we need to inject the style into the Shadow DOM. We can use the same trick with the raw-loader to do that:

import style from "./app.element.css";
import template from "./app.element.html";
export class AppElement extends HTMLElement {
public static observedAttributes = [];
connectedCallback() {
const title = 'custom-webpack';
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<style>${style}</style>` + template.replace('${title}', title);
}
}
customElements.define('custom-webpack-root', AppElement);
view raw app.element.ts hosted with ❤ by GitHub
module.exports = (config, context) => {
return {
config,
module: {
config.module,
rules: [
config.module.rules,
{
test: /\.html$/i,
use: 'raw-loader',
},
{
test: /\.css$/i,
use: 'raw-loader',
},
],
},
};
};
view raw webpackConfig.js hosted with ❤ by GitHub
Code snippet 3: The configuration for loading CSS raw and its usage. This will fail because of nx’s other loaders.

The code in Code snippet 3 won’t work. The reason is that NX already has loaders for CSS and they clash with our raw loader. You will get the following error:

Figure 3: The error shown when trying to add the raw-loader for CSS files.

Logging the original config.module.rules shows us this picture:

Figure 4: The output of logging the NX default webpack config.module.rules.

We can see in Figure 4 that the CSS rules we don’t want anymore (unless we are using a CSS compiler like SASS or LESS) are the second of a 2 members rules array. Hence, we can change our code to just use the first rule and ignore the original CSS rules:

import style from "./app.element.css";
import template from "./app.element.html";
export class AppElement extends HTMLElement {
public static observedAttributes = [];
connectedCallback() {
const title = 'custom-webpack';
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<style>${style}</style>` + template.replace('${title}', title);
}
}
customElements.define('custom-webpack-root', AppElement);
view raw app.element.ts hosted with ❤ by GitHub
module.exports = (config, context) => {
return {
config,
module: {
config.module,
rules: [
config.module.rules[0],
{
test: /\.html$/i,
use: 'raw-loader',
},
{
test: /\.css$/i,
use: 'raw-loader',
},
],
},
};
};
view raw webpackConfig.js hosted with ❤ by GitHub
Code snippet 4: Just changing line 7 in webpackConfig.js to not spread the original array but just take the TS rule.

And now our app is working!

Summary

In this article we saw how to customize the webpack configuration in a project inside an NX monorepo. The two use cases we dealt with were solved by adding a loader. In the first case we just added a loader and in the second case, we had to replace the loader for the file type.

It is very possible your application will need a more refined customization, or you’d might want to change the plugins or even the webpack’s output or optimization behavior. I hope you now have more clue on how to do it.

Sign up to my newsletter to enjoy more content:

Leave a Reply

Your email address will not be published. Required fields are marked *