Managing ASP.NET Core MVC front-end dependencies with npm and webpack (part 1)
Tags: aspnet, mvc, webpack, npm
Published: 2021-04-09 12:23:37

.NET 8 provides a couple of great project templates to get you up and running quickly. However, the MPA (multiple-page application) is not getting much attention. Front-end packages are outdated, and you can’t add new JS libraries using modern practices ([LibMan](https://learn.microsoft.com/en-us/aspnet/core/client-side/libman/?view=aspnetcore-8.0) doesn't count 😉).
Starting out is easy though: either you choose the `web` template, which gives you an almost empty project, or you can opt for more boilerplate by choosing the `mvc` or `webapp` template.

Both have their (dis)advantages:
* `dotnet new web`: this gives you… pretty much
  nothing. That’s not necessarily bad if 
  you’re up for configuring everything from 
  scratch.
* `dotnet new mvc` (or `webapp`): gives you an MPA
  right off the bat, but the front-end libraries
  are outdated and bundled _with_ the project,
  which doesn’t provide a lot of room for change.
  (`webapp` results in a page-based project while 
  `mvc` produces a Model-View-Controller project)

Getting your front-end configured just the way you want can be challenging. There are no project templates that include a clean npm/webpack setup. If you want to use a SPA framework, you can choose between `angular` and `react`. But what if you use a different framework (such as [Vue](https://vuejs.org/) or [Svelte](https://svelte.dev/)!) or even no framework? Well, this guide can help you set that up!
(Note: In .NET 8, the `react` and `angular` templates were migrated from the .NET SDK to Visual Studio.)

### What we’re about to do
* We’re going to set up an ASP.NET Core **MVC**
  project with custom front-end dependencies.
  It’ll be a multi-page app that is tailored to 
  our needs. More specifically…
* We’ll be making sure Bootstrap is a
  _dependency_ of the project instead of a 
  _part_ of the project. So, our project tree 
  will no longer be cluttered with a bunch of 
  minified JS and MAP files.
* We’ll be kicking out JQuery like it’s 2014!
  It’ll only be referenced from ASP.NET’s form 
  validation since [it still depends on it](https://github.com/dotnet/aspnetcore/issues/8573).
* Using this setup, we'll be able to use [TypeScript](https://www.typescriptlang.org/)! 🎉
* The dependencies and build process will be set 
  up using **npm** and **webpack**. Any code that we write will be processed by 
  a build task. We’ll move all `.js`, `.ts`, and `.css` 
  files into a separate `src` directory. Only the 
  _build artifacts_ will end up in `wwwroot`.

### Why we’re doing it
First and foremost, the MVC template is much too firm and inflexible. Adding new front-end libraries to the project is a bit of an undertaking and that shouldn’t be the case.  
Adding a NuGet package to the project (thankfully) doesn’t result in a bunch of DLLs in the project tree. Neither should be the case when adding a library for the front-end. We also want control over the exact version numbers that are used, just like with NuGet packages.

Along the way, we’ll get a better insight into how webpack can be used outside of a SPA!

Let’s get to it!

## Creating the project
What you’ll need:
* A [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0). Previous versions of .NET and .NET Core will work fine as well. I'm using 8.0.203 and have previously used .NET 5 and 6 as well as .NET Core for this setup.
* [NodeJS with bundled npm](https://nodejs.org/)
  (or yarn), I’m using 21.4.0.
* Optionally, an IDE. For now, all you need is a 
  functional command line!

Let’s create a new MVC project. Navigate to a new empty directory and enter the following:

```
$ dotnet new mvc
```

If you’re using git then go ahead and create a `.gitignore` file. Thankfully, there's a separate .NET template to take care of this. Still from within your project directory, enter:

```
$ dotnet new gitignore
```

Launch the project with:

```
$ dotnet run
```

Check the output (or `Properties/launchSettings.json`) to see which port number has been [generated](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-6.0?view=aspnetcore-8.0#template-generated-ports-for-kestrel) for your project and navigate to `localhost` to have a look.

This startup project can be found on Gitlab as [the first version](https://gitlab.com/kdg-ti/integratieproject-1/guides/net8npmwebpack/-/tags/version-1) of [Net8NpmWebpack](https://gitlab.com/kdg-ti/integratieproject-1/guides/net8npmwebpack).

## Front-end dependencies
Next up, create a new directory called `ClientApp` at the root of your MVC project. Inside this new directory, create a file called `package.json`. Give it the following content:

```json
{
    "name": "net8npmwebpack",
    "description": "ASP.NET Core MVC project with npm and webpack front-end configuration.",
    "repository": "https://gitlab.com/kdg-ti/integratieproject-1/guides/net8npmwebpack",
    "license": "MIT",
    "version": "5.0.0",
    "dependencies": {
        "@popperjs/core": "^2.11.8",
        "jquery": "^3.7.1",
        "jquery-validation": "^1.20.0",
        "jquery-validation-unobtrusive": "^4.0.0",
        "bootstrap": "^5.3.3",
        "bootstrap-icons": "^1.11.3"
    },
    "devDependencies": {
        "webpack": "^5.91.0",
        "webpack-cli": "^5.1.4",
        "css-loader": "^6.10.0",
        "style-loader": "^3.3.4"
    },
    "scripts": {
        "build": "webpack"
    }
}
```

You’ll want to change the values of the first five fields if you apply these steps to an existing project.

Apart from the obvious fields, we’re specifying:
* `dependencies`: includes Bootstrap 5, JQuery,
  and Popper (for popup placement, used by Bootstrap)
* `devDependencies`: webpack and webpack-related 
  loaders. We’ll need these to bundle our front-end code.
* `scripts`: a single command that invokes the 
  webpack bundling process

Go ahead and run the following command from inside the `ClientApp` directory:

```
$ npm install
```

A new `node_modules` directory will have popped up containing a massive amount of JS and CSS files. Consider this directory to be like the `bin` and `obj` directories (or, better yet, the NuGet package cache). Although it doesn’t contain any binaries, its content is still a load of dependencies we refer to from within our code.

… but we don’t _have_ any code yet. Let’s add some by creating a `src` directory inside `ClientApp` and adding both a `js` and a `css` directory at that location. Create a new file called `site.js` in the `js` directory and **move** the contents of _both_ `wwwroot/css/site.css` and `Views/Shared/_Layout.cshtml.css` to `ClientApp/src/css`. Like so:

```bash
ClientApp/
├── package-lock.json
├── package.json
└── src
    ├── css
    │   └── site.css  # contents of wwwroot/css/site.css and Views/Shared/_Layout.cshtml.css
    └── js
        └── site.js   # newly created
```

Cleanup is important so take your time remove directories `wwwroot/css` and `wwwroot/js` entirely and to remove `Views/Shared/_Layout.cshtml.css` as well.

In `site.js` we will write custom JavaScript code that’s relevant to the entire site. Additionally, we'll use this file to import all site-wide dependencies:

```javascript
// JS Dependencies: Popper, Bootstrap & JQuery
import '@popperjs/core';
import 'bootstrap';
import 'jquery';
// Using the next two lines is like including partial view _ValidationScriptsPartial.cshtml
import 'jquery-validation';
import 'jquery-validation-unobtrusive';

// CSS Dependencies: Bootstrap & Bootstrap icons
import 'bootstrap-icons/font/bootstrap-icons.css';
import 'bootstrap/dist/css/bootstrap.css';

// Custom JS imports
// ... none at the moment

// Custom CSS imports
import '../css/site.css';

console.log('The \'site\' bundle has been loaded!');
```

Lines 2 to 9 will import code from within `node_modules`. We’re importing Bootstrap’s Javascript, Popper, and CSS as well as a couple of JQuery libaries that are used by ASP.NET Core’s validation scripts.
All of this is done using [ECMAScript 6 modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import), a modern approach to importing JS files from other JS files or even CSS files from JS files.

Now… how can we possibly get this code into a user’s browser? The file we just created contains only 17 lines of code, but it imports a small list of libraries and some custom CSS code. Do we just throw `node_modules` at our users? Of course not! This directory is so large that we need to carefully filter, bundle, and host only those parts that are needed at runtime.

… this is where webpack comes in!

### Building the bundle
Create a new file called `webpack.config.js` and place it in the `ClientApp` directory. Give it the following content:

```javascript
const path = require('path');

module.exports = {
    entry: {
        site: './src/js/site.js'
    },
    output: {
        filename: '[name].entry.js',
        path: path.resolve(__dirname, '..', 'wwwroot', 'dist'),
        clean: true
    },
    devtool: 'source-map',
    mode: 'development',
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            },
            {
                test: /\.(png|svg|jpg|jpeg|gif|webp)$/i, 
                type: 'asset'
            },
            {
                test: /\.(eot|woff(2)?|ttf|otf|svg)$/i,
                type: 'asset'
            }
        ]
    }
};
```

Without going into too much detail, we’re specifying that a bundle should be created based on `src/js/site.js` and that this bundle should be called `site.entry.js` (the `[name]` placeholder is replaced with “site”).

webpack is smart enough to figure out what should be included in the bundle and bases its decision-making on what we do in `site.js`. For example, if we import JQuery, then JQuery will be included in the bundle. Even CSS files such as those from Bootstrap will be included in the bundle if we choose to import them from within `site.js`. (In the `rules` section at the bottom, we specify how those non-JS files are handled.)

Ok, let’s build this using the following command from within the `ClientApp` directory:

```bash
$ npm run build
```

Basically, this executes the `webpack` command as can be seen in the `scripts` section of `package.json` above.  
This _build_ results in two new files:

```bash
wwwroot/dist/
├── site.entry.js
└── site.entry.js.map
```

Now we can include those from within our HTML. Open `Views/Shared/_Layout.cshtml` and replace all `script` and `link` tags (even those at the bottom!) with a single line:

```diff
 <!DOCTYPE HTML>
 <html lang="en">
 <head>
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>@ViewData["Title"] - Net8NpmWebpack</title>
+    <script src="~/dist/site.entry.js" defer></script>
-    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
-    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
-    <link rel="stylesheet" href="~/Net8NpmWebpack.styles.css" asp-append-version="true" />
 </head>
 <body>
     <header>
         <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
             <div class="container-fluid">
                 <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Net8NpmWebpack</a>
                 <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                         aria-expanded="false" aria-label="Toggle navigation">
                     <span class="navbar-toggler-icon"></span>
                 </button>
                 <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                     <ul class="navbar-nav flex-grow-1">
                         <li class="nav-item">
                             <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                         </li>
                         <li class="nav-item">
                             <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                         </li>
                     </ul>
                 </div>
             </div>
         </nav>
     </header>
     <div class="container">
         <main role="main" class="pb-3">
             @RenderBody()
         </main>
     </div>

     <footer class="border-top footer text-muted">
         <div class="container">
             &copy; 2024 - Net8NpmWebpack - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
         </div>
     </footer>
-    <script src="~/lib/jquery/dist/jquery.min.js"></script>
-    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
-    <script src="~/js/site.js" asp-append-version="true"></script>
     @await RenderSectionAsync("Scripts", required: false)
 </body>
 </html>
```

`defer` indicates that execution of this file should be delayed until the page is fully loaded. That’s a modern alternative to putting all `script` tags at the bottom of a document to make sure that the document is fully downloaded before the scripts start scanning the DOM.

While we're at it, let's quickly throw in a [Bootstrap Icon](https://icons.getbootstrap.com/). In `_Layout.cshtml`, scroll down to the footer and change the following:

```diff
- &copy; 2024 - Net8NpmWebpack - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
+ <i class="bi bi-c-circle"></i> 2024 - Net8NpmWebpack - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
```

Time to delete the last bit of clutter from our project tree:

```
$ rm -rf wwwroot/lib
```

If you’re as obsessed with useless comments as I am then now’s the time to remove the comment in `site.css`. Whatever minification reference is being made there, we’re not going down that path anyway.  
And if you’re using git, add `wwwroot/dist/` to your `.gitignore`.

Alright, time to check it out!

```bash
$ dotnet run
```

Open `localhost` (check that port again) in a browser and check the browser console (F12). You should see the message _“The ‘site’ bundle has been loaded!”_.

How cool is that? All of the front-end’s dependencies are now downloaded, built, and bundled dynamically. The build system is extensible and all dependencies are easily managed and replaceable if needed!

The project so far can be found on Gitlab as [version 2](https://gitlab.com/kdg-ti/integratieproject-1/guides/net8npmwebpack/-/tags/version-2) of [Net8NpmWebpack](https://gitlab.com/kdg-ti/integratieproject-1/guides/net8npmwebpack).

## TypeScript
Now that we've done the heavy lifting, the world is our oyster! Let's add [TypeScript](https://www.typescriptlang.org/).  
In `package.json`, add a couple of `devDependencies`:

```diff
     "devDependencies": {
+        "@tsconfig/recommended": "^1.0.5",
+        "@types/bootstrap": "^5.2.10",
+        "ts-loader": "^9.5.1",
+        "typescript": "^5.4.3",
         "webpack": "^5.91.0",
```

In `ClientApp`, add a `tsconfig.json` file with this content:
```json
{
    "extends": "@tsconfig/recommended/tsconfig.json",
    "compilerOptions": {
        "module": "es6",
        "target": "es6",
        "allowJs": true,
        "moduleResolution": "node"
    }
}
```

Rename your `js` directory and `.js` files. You should end up with something like this:

```bash
ClientApp/
└── src
    └── ts
        └── site.ts
```

Now, only webpack needs to be configured to handle TypeScript. In `webpack.config.js` apply these changes:

```diff
     entry: {
-        site: './src/js/site.js'
+        site: './src/ts/site.ts'
     },
     output: {
         filename: '[name].entry.js',
         path: path.resolve(__dirname, '..', 'wwwroot', 'dist'),
         clean: true
     },
     devtool: 'source-map',
     mode: 'development',
+    resolve: {
+        extensions: [".ts", ".js"],
+        extensionAlias: {'.js': ['.js', '.ts']}
+    },
     module: {
         rules: [
+            {
+                test: /\.ts$/i,
+                use: ['ts-loader'],
+                exclude: /node_modules/
+            },
             {
```

## Sass
We can add [Sass](https://sass-lang.com/) to make our styling life easier. 🕶️  
In `package.json`, we need two more `devDependencies`:
```diff
         "@types/bootstrap": "^5.2.10",
+        "sass": "^1.72.0",
+        "sass-loader": "^14.1.1",
         "ts-loader": "^9.5.1",
```

Let's go for this directory structure and file name:
```bash
ClientApp/
└── src
    └── scss
        └── site.scss
```

As a kind of smoke test, we can introduce a Sass variable in `site.scss`:

```diff
+ $button-bg-color: #1b6ec2;

 // More styling here

 .btn-primary {
   color: #fff;
-  background-color: #1b6ec2;
+  background-color: $button-bg-color;
   border-color: #1861ac;
 }

 .nav-pills .nav-link.active, .nav-pills .show > .nav-link {
   color: #fff;
-  background-color: #1b6ec2;
+  background-color: $button-bg-color;
   border-color: #1861ac;
 }

 // Even more styling follows
```

In `site.ts`, we must import the correct (renamed) style sheet:
```diff
  // Custom CSS imports
- import '../css/site.css';
+ import '../scss/site.scss';
```

And finally, in `webpack.config.js` we can enable Sass file processing:
```diff
             {
-                test: /\.css$/,
-                use: ['style-loader', 'css-loader']
+                test: /\.s?css$/,
+                use: ['style-loader', 'css-loader', 'sass-loader']
             },
```

Give the application another spin. From `ClientApp`, don't forget to install the new NPM dependencies and build a new bundle:

```bash
$ cd ClientApp
$ npm i
$ npm run build
$ cd ..
$ dotnet run
```

This concludes the first part of this guide. [Here, you can find the third version of this project.](https://gitlab.com/kdg-ti/integratieproject-1/guides/net8npmwebpack/-/tags/version-3)

## What’s next?
We’re not quite finished. There are two important things on our TODO list:
* **Performance!** Try switching quickly between 
  different pages in the SPA. You’ll see a delay 
  before Bootstrap’s CSS gets applied. This is caused 
  by a large amount of CSS getting plugged into 
  the page at runtime. This is the most urgent 
  thing to fix, _user experience_ is just too 
  important!
* **Building the project as a whole**. At this 
  point building the .NET project doesn’t trigger 
  a rebuild for the front-end. How annoying! :)

[In part 2 we’ll be tackling the performance issue and streamlining the build process.](https://dev.to/larswillemsens/managing-asp-net-core-mvc-front-end-dependencies-with-npm-and-webpack-part-2-3acp)