Install NPM Packages in a .NET Solution using Gulp

.NET’s NuGet package manager is fantastic, but let’s face it—its client-side package management is awful. There have been some attempts at managing client-side packages with LibMan, but my personal experience is that it is very flaky and not very feature rich.

Today, I am going to show you how to use Node Package Manager (NPM) in a .NET project. And I am going to fully automate the process so that client-side libraries are restored just like NuGet packages, so your local development process is simply build and run.

A video run through of this tutorial can be found here.


Installing NPM Packages

First, ensure that you have Node.js and NPM (the package manager for Node.js) installed. See: https://nodejs.org/en

Generate a package.json file to your project by navigating to the root of your project folder and running:

npm init -y

We will now modify the package.json file, specifying that the type is module (the modern JavaScript module system) and add a dependencies section and list any packages that you wish to install. In this example, we will install Bootstrap and jQuery:

{
  "name": "example",
  "type": "module",
  "version": "1.0.0",
  "description": "",
  "main": "",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Terry Mace",
  "dependencies": {
    "bootstrap": "5.2.x",
    "jquery": "3.x"
  }
}

Now if you run npm install, the dependencies listed will be installed. However, these will be installed in a node_modules folder, and CSS/JS files will not be accessible from a browser. By default, static files are only accessible from wwwroot.


Copy Files From node_modules to wwwroot

To copy your npm files to wwwroot, we will use Gulp. Gulp is a JavaScript task runner.

In your package.json file, add the following devDependencies:

{
  "name": "example",
  "type": "module",
  "version": "1.0.0",
  "description": "",
  "main": "",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Terry Mace",
  "dependencies": {
    "bootstrap": "5.2.x",
    "jquery": "3.x"
  },
  "devDependencies": {
    "gulp": "^5.0.0",
    "gulp-clean": "^0.4.0"
  }
}

Next, create a gulpfile.js file in the root of your project and add the following:

import gulp from "gulp";
import clean from "gulp-clean";

const paths = {
    bootstrap: {
        src: "node_modules/bootstrap/**/*",
        dest: "wwwroot/lib/bootstrap/"
    },
    jquery: {
        src: "node_modules/jquery/**/*",
        dest: "wwwroot/lib/jquery/"
    }
};

// Task to clean the wwwroot/lib folder
gulp.task("clean", function () {
    return gulp.src("wwwroot/lib/", { read: false, allowEmpty: true })
        .pipe(clean());
});

// Task to copy Bootstrap files
gulp.task("copy-bootstrap", function () {
    return gulp.src(paths.bootstrap.src)
        .pipe(gulp.dest(paths.bootstrap.dest));
});

// Task to copy jQuery files
gulp.task("copy-jquery", function () {
    return gulp.src(paths.jquery.src)
        .pipe(gulp.dest(paths.jquery.dest));
});

// Combined copy task
gulp.task("copy", gulp.parallel("copy-bootstrap", "copy-jquery"));

// Task to clean and then copy files
gulp.task("cleanAndCopy", gulp.series("clean", "copy"));

Let’s break down what we are doing here.

  1. We import the required modules: gulp and gulp clean. We’re using ES module syntax (import instead of require), which is a modern approach to JavaScript module handling. If your environment doesn’t support ES modules, you might need to use require() instead.
  2. we define an object called paths that holds the source and destination paths for Bootstrap and jQuery:
  3. Before we copy new files, we remove any outdated or unnecessary files from the destination folder by creating a task called clean:
    • gulp.src(paths.dest, { read: false, allowEmpty: true }) selects the destination folder:
      • read: false improves performance since we don’t need to read file contents before deleting.
      • allowEmpty: true ensures that Gulp doesn’t throw an error if the directory is already empty.
    • .pipe(clean()) actually removes the files.
  4. We define a task called copy-bootstrap to copy Bootstrap files from node_modules to our wwwroot/lib folder:
    • gulp.src(paths.bootstrap.src): Selects all Bootstrap files.
    • .pipe(gulp.dest(paths.bootstrap.dest)): Copies them to the destination folder.
  5. We do the same thing for jQuery with a task called copy-jquery
  6. Since copying Bootstrap and jQuery are independent tasks, we run them in parallel using gulp.parallel which runs both tasks simultaneously.
  7. We create a task called clean-and-copy that first cleans the wwwroot/lib directory and then copies the necessary files.

Now if we call gulp clean-and-copy the installed files will be copied from node_modules to wwwroot\lib. To have this task automatically triggered after an npm install, we add a postinstall script to our package.json file where we call gulp clean-and-copy:

{
  "name": "example",
  "type": "module",
  "version": "1.0.0",
  "description": "",
  "main": "",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "postinstall": "gulp clean-and-copy"
  },
  "author": "Terry Mace",
  "dependencies": {
    "bootstrap": "5.2.x",
    "jquery": "3.x"
  },
  "devDependencies": {
    "gulp": "^5.0.0",
    "gulp-clean": "^0.4.0"
  }
}

Now when we run npm install, the following will happen:

  1. Any packages specified in your package.json file will be installed in the node_modules directory.
  2. All files in the wwwroot/lib/ folder are deleted.
  3. The files specified in the copy task are copied from node_modules into their respective locations in wwwroot/lib/.

Note the clean step in the workflow. Deleting all of the files within the lib folder ensures that if you change your package version or remove a package from your package.json, this is reflected in your lib folder.

If you are using git, it is advisable that you add both node_modules/ and wwwroot/lib/ folders to your .gitignore to prevent committing them to source control. This can be done by adding:

**/node_modules
**/wwwroot/lib

Automating NPM Install

So, we can now install NPM packages in our project and have them moved to wwwroot, but we still have to manually call npm install. This may be a perfectly fine workflow for your use case. However, if you want to automate this call, this can be done by adding a build event to run npm install. Do this cautiously though, because npm install will be called every time you build your project which will add load time. I will now outline how to do this.

In the solution explorer, right click on the project contain the package.json file and select properties. The select Build Event and in the pre-build event input add npm install.


Conclusion

We now have Node packages being installed in a .NET project using NPM.

  • No Manual Steps: Developers don’t have to remember to run npm install.
  • Fully Automated: Integrated seamlessly into the build process.

By following these steps, you can maintain client-side dependencies just like NuGet packages and avoid the headaches of manually restoring front-end libraries. Enjoy your streamlined .NET + NPM workflow!

Comments are closed