Add Snowpack to ASP.NET Core Web App

alt=

Introduction

I stumbled on Snowpack while searching for a solution to resolve a ParcelJS 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.

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

Snowpack v3.0 introduced 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.

  2. Create bootstrap.js file under Client/js folder and insert import bootstrap from 'bootstrap'; to the file.

  3. Create jquery.js file under Client/js folder and insert import jquery from 'jquery'; to the file.

  4. 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=

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:

"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.

alt=

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.

"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=

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

<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:

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=

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:

<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.

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.

"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:

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

You customize Snowpack build process using build plugins.

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

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::

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.

{
  "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:

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=”

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 or listen to the podcast

That’s all for this post.