Add Snowpack to ASP.NET Core Web App
Tags: snowpack, aspnetcore, webdev, tooling
Published: 2021-03-19 01:42:19

---
title: "Add Snowpack to ASP.NET Core Web App"
published: true
description: 
tags: ["snowpack", "aspnetcore", "webdev", "tooling"]
canonical_url: https://kags.me.ke/post/add-snowpackjs-to-asp.net-core-web-app/
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i8dyhir19atwgoyqucr0.png
---

## Introduction

I stumbled on [Snowpack](https://www.snowpack.dev/) while searching for a solution to resolve a [ParcelJS](https://github.com/parcel-bundler/parcel) error.

Building my application assets with ParcelJS worked without errors but when running the application, none of the JS scripts were running.  Upon checking the browser console window, I was getting the following error: **Uncaught (in promise) Error: Cannot find module**. I never managed to resolve the error but Snowpack appeared in one of the searches.

A quick look at the following two paragraphs on Snowpack's landing page and I made a decision to replace ParcelJS with Snowpack.

>>Snowpack is a lightning-fast frontend build tool, designed for the modern web. It is an alternative to heavier, more complex bundlers like webpack or Parcel in your development workflow. ...

>>Once you try it, it's impossible to go back to anything else.

Snowpack builds individual files and caches the built files. Whenever any cached file changes, then Snowpack rebuilds that single file alone. Snowpack will also rebuild and cache any new files added to a project. Snowpack runs through your _package.json_ file application dependencies builds and converts the dependencies to JavaScript ESM modules where necessary and places these modules in the build output folder. File bundling is an opt-in using Snowpack's plugins.

In this post, I go through the steps of adding Snowpack to the build and publish process of an ASP.NET Core web application. I create a new application but the process is also applicable to an existing project.

## Prerequisites

Make sure you have installed downloaded and installed [.NET SDK](https://dotnet.microsoft.com/download/dotnet-core).

You can verify that .NET is installed by opening a new terminal window and running the command `dotnet`.

Snowpack v3.0 introduced [**Streaming Imports**](https://www.snowpack.dev/guides/streaming-imports) which fetches imported packages on-demand thereby  eliminating  dependency on package managers like npm or yarn. In this post, I will stick with the traditional way of managing packages therefore make sure you have **Node.js** and **npm** or **yarn** installed.

## Create Web Application

If you have an existing ASP.NET Core web application, you can skip this step. Also replace any mention of  **SnowpackTest** project with the name of your  project.

Open terminal window and run the following command to create a new web application `dotnet new webApp -o SnowpackTest`

Navigate to the project folder by running `cd SnowpackTest`

Open the project using your preferred source code editor. If you are using Visual Studio Code (what I will use in this post) then run `code .`

## Create Snowpack project root

Create _Client_ (or use your preferred name) folder in the root of the project. This will serve as the root project for Snowpack.

Move _css_ and _js_ folders from _wwwroot_ to _Client_ folder.

Move _favicon.ico_ to the root of _Client_ folder.

Open VS Code terminal (select **View**, **Terminal** from menu). This will open at the project's root folder.

Change directory to the _Client_ folder by running `cd Client` command.

Create and empty _package.json_ file by running `npm init --yes` from the terminal.`--yes` allows the command to generate the file without prompting you for any required data.

Install dependencies contained within the _lib_ folder  under _wwwroot_ by running `npm i bootstrap popper.js jquery jquery-validation jquery-validation-unobtrusive` command

Install Snowpack as a dev dependency `npm install --save-dev snowpack@3.0.13`

## Re-Create Files in _wwwroot/lib_ Folder

We will re-create files contained within the _wwwroot/lib_ folder under _Client_ folder. The files we create are going reference files located within the _node\_modules_ folder.

1. Create _bootstrap.css_ file under _Client/css_ folder. Update the file with the following `@import "bootstrap/dist/css/bootstrap.css";` statement.

1. Create _bootstrap.js_ file under _Client/js_ folder and insert `import bootstrap from 'bootstrap';` to the file.

1. Create _jquery.js_ file under _Client/js_ folder and insert `import jquery from 'jquery';` to the file.

1. I will combine the jQuery validation files but you can create individual separate files. Create _jquery-validation.js_ file under _Client/js_ folder. Insert the following two statements to the file `import 'jquery-validation';` and `import 'jquery-validation-unobtrusive';`

The structure of your _Client_ folder after completing above steps should resemble the following:

![Alt "Client folder with updated files"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ujka0ggbvbipd4y73o3q.png)

After the above steps, you can delete _wwwroot_ folder. We will configure Snowpack to re-create the folder for us during the build process.

## Configure Snowpack

Snowpack supports the following configuration files:

1. _package.json_ - the file should contain a Snowpack config object (`"snowpack": {...}`)

2. _snowpack.config.js_ - this is a Javascript file exporting a Snowpack config object (`module.exports ={ ... }`)

3. _snowpack.config.json_ - A JSON file containing config (`{ ... }`)

You can also use command-line interface (CLI) flag to provide a different config file (`snowpack --config ./path/to/snowpack.deploy.json`). CLI flags take precedence over other configuration file settings.

Open _package.json_ located within _Client_ folder and add a Snowpack config object above the `dependencies` config object or anywhere within the file.

Update the config object with the following:

```json
"scripts": {
  "build-snowpack": "snowpack build"
},
"snowpack": {
    "exclude": [
      "*.js",
      "*.json"
    ],
    "buildOptions": {
      "out": "../wwwroot/dist"
    }
},
```

We want to exclude any configuration files located within the Snowpack project root from the build process by using the `exclude` directive. By default, Snowpack excludes files within _node\_modules_ folder.

`out` is the local directory where Snowpack will output our final build.

## Test Snowpack Build

Open VS Code terminal if not already open.

If the terminal opens within the web application root folder, run `npm run build-snowpack --prefix ./Client` command. `--prefix` points **npm** to the _package.json` file location.

If the terminal is on _Client_ folder, run `npm run build-snowpack` command.

After the command completes, you get an output similar to the following:  

    [snowpack] ! building source files...    
    [snowpack] ✔ build complete [0.08s]    
    [snowpack] ! building dependencies...    
    [snowpack] ✔ dependencies ready! [2.83s]    
    [snowpack] ! verifying build...    
    [snowpack] ✔ verification complete [0.00s]    
    [snowpack] ! writing build to disk...    
    [snowpack] watching for changes...    

The _wwwroot_ folder will be re-created at the root of the project.  CSS and JS files within _Client_ folder will be copied to the _wwwroot/dist_ folder under respective folders.

Snowpack will also create a _\_snowpack_ folder within _wwwroot/dist_ folder. This is the folder for Snowpack metadata. You can set a different name for this folder by setting the value under [buildOptions.metaUrlPath](https://www.snowpack.dev/reference/configuration#buildoptions.metaurlpath).

![Alt "Snowpack created wwwroot folder files"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h7ly0yfovbvoyas8ymff.png)

Any CSS and JS files within _wwwroot/dist_ folder that reference packages from the _node\_modules_  get updated during  the build process  to point to the _\_snowpack_ folder. For example _Client/css/bootstrap.css_ contains `@import "bootstrap/dist/css/bootstrap.css";`. After the build process, _wwwroot/dist/css/bootstrap.css_ will contain `import "../_snowpack/pkg/bootstrap/dist/css/bootstrap.css";`.

## Update JS and CSS File References

Open _\_Layout.cshtml_ file located within _Pages/Shared_ folder. Replace  CSS link references with the following:

    <link rel="shortcut icon" href="~/dist/favicon.ico">
    <link rel="stylesheet" href="~/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/dist/css/site.css" />

At the bottom of the file, update the script references with the following:

    <script type="module" src="~/dist/js/jquery.js"> </script>
    <script type="module" src="~/dist/js/bootstrap.js"></script>
    <script type="module" src="~/dist/js/site.js"></script>

Open `_ValidationScriptsPartial.cshtml` within  _Pages/Shared_ folder and replace the contents with  `<script type="module" src="~/dist/js/jquery-validation.js"></script>`

By declaring `type="module"` on our script tags, we tell the browser that we are using JavaScript's ES Modules (ESM). Failing to add `type="module"` on the scripts will display an **Uncaught SyntaxError: Cannot use import statement outside a module** on the browser console window when the application runs.

## Configure Application Build and Changes Watch

Before we start watching Snowpack build process, let's update our _package.json_ file with a command for watching the build process. Open _./Client/package.json_ file and append `"watch-snowpack": "snowpack build --watch"` to the  scripts object. `--watch will ensure that the build process does not complete and exit but will continue watching for any file changes that Snowpack is to build.

```json
"scripts": {
   "build-snowpack": "snowpack build",
   "watch-snowpack": "snowpack build --watch"
},
```

Open VS Code integrated terminal if not already open. Make sure you are at  the root of project. If you  are on _Client_ folder, run `cd ..` command to take  you back to the project root folder.

Run `dotnet watch run` command. This will build the project and continue watching for any changes to the files.

Split the terminal window by right clicking the terminal window and selecting **Split** from the context menu.

On the new terminal tab, run `npm run watch-snowpack --prefix ./Client` command. Snowpack will build all the files and continue monitoring files for any changes.

![Alt "dot net and snowpack watch split screen"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xv8ah5lfadf9fzsdtepd.png)

## Test Snowpack Build and Watch

In a web browser, navigate to **https://localhost:5001/** to display the default welcome page.

Open _Pages/Index.cshtml_ in the code editor and replace `<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>` with the following

```html
<div class="card w-50 mx-auto">
   <div class="card-body">
      <h5 id="name" class="card-title">Name</h5>
        <div class="card-text">Email: <span id="email" class="small font-weight-bold"></span></div>
        <div class="card-text">phone: <span id="phone"  class="small font-weight-bold"></span></div>
        <div class="card-text">Website: <span id="website" class="small font-weight-bold"></span></div>
        <button id="fetchUser" class="btn btn-primary mt-5">Fetch User</button>
      </div>
</div>
```

Save the  file and .NET should rebuild the project for you.

We are going to fetch dummy random users from **https://jsonplaceholder.typicode.com** and display them on the homepage.

Open _Client/site.js_ and update the file with the following code:

```js
 const fetchUser = document.getElementById('fetchUser');
 fetchUser?.addEventListener('click', async function() {
    const userId = Math.floor(Math.random() * (10 - 1) + 1);
    const url = `https://jsonplaceholder.typicode.com/users/${userId}`;

    const response = await fetch(url);
    if (response.ok) {
      const user = await response.json();
      if (user) {
        document.getElementById('name').innerHTML = user.name;
        document.getElementById('email').innerHTML = user.email;
        document.getElementById('phone').innerHTML = user.phone;
        document.getElementById('website').innerHTML = user.website;
      }
    }
 })
```

Saving the changes, you will observe that Snowpack displays a `[snowpack] File changed...` message on the terminal.

Refresh the page on the browser and the page gets replaced with blank user data.

If you click on the **Fetch User** button, the homepage should now display data for a random user.

![Alt "Updated home page with random user"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ekhm5hgwa7gjvp2nr4zm.png)

Creating a new JS or CSS file within the _Client_ folder should  trigger a build and the new  file appended to the _wwwwroot_ folder. Renamimg a file will however leave the previous file within the _wwwroot_ folder. This should not be an issue as during project publishing, we will perform a clean build.

Kill all terminal tabs by right clicking on each open terminal tab and selecting **Kill Terminal** from the context menu.

## Prepare Project for Publishing

The project publishing process will involve the following 3 tasks:
  1. Embed Snowpack build process to the .NET publishing process
  2. Minify our CSS and JS files
  3. Remove un-used CSS classes

### 1. Embed Snowpack build process to the .NET publishing process

Open the project file _SnowpackTest.csproj_ at the root of the project and update as follows:

```xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>

    <ClientRoot>$(ProjectDir)Client\</ClientRoot>
    <DefaultItemExcludes>$(DefaultItemExcludes);$(ClientRoot)/*.*;$(ProjectDir)*.Development.json</DefaultItemExcludes>

  </PropertyGroup>

  <Target Name="NpmRunPublish" BeforeTargets="Publish">
    <Exec WorkingDirectory="$(ProjectDir)" Command="npm run publish --prefix ./Client" />
  </Target>

</Project>
```

`<ClientRoot>$(ProjectDir)Client\</ClientRoot>` defines a property and value that will be referenced later in the project file.

`$(ProjectDir)` and `$(DefaultItemExcludes)` are [predefined MSBuild properties](https://docs.microsoft.com/en-us/cpp/build/reference/common-macros-for-build-commands-and-properties).

`DefaultItemExcludes` defines patterns for files that MSBuild should exclude from the build process.

`$(ClientRoot)` references the property created earlier within the project file.

Within _Client/package.json_, add `dotnet-publish` command under **scripts** object.

```json
"scripts": {
  "build-snowpack": "snowpack build",
  "watch-snowpack": "snowpack build --watch",
  "publish": "NODE_ENV=production snowpack build --config snowpack.publish.json"
},
```

`publish` uses CLI to tell Snowpack the configuration file (_snowpack.publish.json_) to use.

`NODE_ENV=production` signals to Node.js that we are running in production and not development environment which is the default.

### 2. Minify CSS and JS files.

Create _Client/snowpack.publish.json_ file and update with the following:

```json
{
  "exclude": [
    "*.js",
    "*.json"
  ],
  "plugins": [
    ["@snowpack/plugin-optimize", {"target" : "es2020"}]
  ],
  "buildOptions": {
    "out": "../publish/wwwroot/dist"
  }
}
```

You customize Snowpack build process using [build plugins](https://www.snowpack.dev/plugins).

Open the integrated terminal and install Snowpack optimize plugin using following command:

```sh
npm install --save-dev --prefix ./Client @snowpack/plugin-optimize
```

The optimize plugin will minify the CSS and JS files.

### 3. Remove Un-used CSS Classes

Removing CSS class names will reduce the size of our CSS files.

Run the following command from the integrated terminal::

```sh
npm install --save-dev --prefix ./Client @snowpack/plugin-postcss postcss postcss-cli @fullhuman/postcss-purgecss
```

If you are running the command from the _Client_ folder, omit the  `--prefix ./Client`:

Update  _Client/snowpack.publish.json_ file to include Snowpack PostCSS plugin.

```json
    {
      "exclude": [
        "*.js",
        "*.json"
      ],
      "plugins": [
        ["@snowpack/plugin-postcss"],
        ["@snowpack/plugin-optimize", {"target" : "es2020"}]
      ],
      "buildOptions": {
        "out": "../publish/wwwroot/dist"
      }
    }
```

Configure PostCSS to remove any unused CSS classes from our CSS files.

Create _Client/postcss.config.js_ file and update with the following:

```js
const purgecss = require('@fullhuman/postcss-purgecss')({
  content: [
    '../Pages/**/*.cshtml',
    './**/*.js',
  ],
  css: [ '../publish/**/*.css'],
});

module.exports = {
  plugins: [
    ...process.env.NODE_ENV === 'production' ? [purgecss] : []
  ]
}
```

`content` refers to the location of files containing CSS class names while `css` is the location of the CSS files. We have also configured PurgeCSS to run when the process environment is set to production.

## Publish .NET Project

To publish the application, run `dotnet publish -c Release -o ./publish` from the terminal window. Make sure you are running the command from the project root folder.

![Alt ".NET publish folder files"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oci0mb594d4vc51xynhf.png)

If you open _site.css_ and _site.js_ within the _publish_, you will observe that the files are now compressed.

Try adding some unused CSS classes within _Client/css/site.css_ and then publish the .NET project. You  will observe that those un-used CSS classes are missing in the published CSS file.

##  Conclusion

This post addressed one way of adding Snowpack build tool to an ASP.NET Core project. I relocated files within default _wwwroot_ folder to a different folder and let Snowpack create the _wwwroot_ folder during the build process. By use of Snowpack plugins, I minified the JS and CSS files and removed any un-used CSS classes from CSS files. Modifying the .NET project file made it possible to embed Snowpack's build process to the .NET project publishing process.

## Snowpack's Origin

You can watch [Snowpack with Fred K. Schott](https://www.youtube.com/watch?v=ZQZmPooJ4cA) or listen to the [podcast](https://www.contributor.fyi/snowpack)


That's all for this post.