9 minute read

I have been recently working on an internal project which allows people to authenticate into a Wi-Fi with Azure AD and other various methods through a captive portal. While trying to maintain a strict policy on what hostnames can be accessed (basically just allowing Azure AD endpoint's and the application server) I noticed that the default ASP.NET Core project setup seems to set a bad example in handling JavaScript libraries in your project.

Back in the days of ASP.NET Core 1.0, all frontend libraries used to be loaded by Bower. This was pretty nice, because it allowed you to keep all the libraries out of the project's source (by using .gitignore) and simply not commiting them.

But Honza, the default project scaffold contains only 4 tiny libraries.... Yeah, but why should those be in my Git repository? Ideally, those will be loaded from an external CDN anyways. My major issue with this is, that it sets a bad example for developers. You don't include Nuget packages in your Git repository, do you? Why would you include these then?

My problem

When I scaffold a new project, what I always do, is remove the local files, only keep the CDN which I then replace with cdnjs, since using a single CDN across an entire project allows your users to fully benefit from HTTP/2 features and so on.

This project is unique with the fact that I don't want to use any CDNs at all. Since captive portal works by the access point/gateway restricting your access to outbound IP addresses with the exception of the captive portal itself and some other exceptions, you probably don't want to whitelist an entire CDNs namespace. That's why I want to have the libraries served from the server (not in my Git tho).

Choosing the package manager

Since Bower is more or less outdated and after evaluating all the other options available out there, I decided to go with NPM. There are multiple other alternatives out there like Yarn, but that would introduce yet another tool into the build machine (Azure App Service in my case) and onto the dev machine as well.

I expect a standard ASP.NET Core dev computer to contain the .NET Core SDK and Node.js with NPM - simply due to building Single Page Applications, using bundlers, Gulp, Grunt, webpack etc.

Storing NPM packages

Next obvious task was to decide where the NPM packages will be stored in the project structure. The most obvious place would be in the project's root (/src/WebApplication1/node_modules/) but that didn't turn out that good. When you decide to use another folder for static files, everything is going to work, except asp-append-version tag (the tiny thingy which makes frontend asset versioning super simple), which I simply wanted there, in case somebody would update the libraries. There are some workarounds, but those are not really ideal I would say.

Unlike with Bower, NPM's package.json doesn't offer any option to specify, where the node_modules folder will be stored - by default and in all cases (I didn't find an option to place it elsewhere), the folder will end up in the same directory. So I put the package.json file into /src/WebApplication1/wwwroot/ folder, gitignored the node_modules folder and added npm install into the build task:

<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build">
  <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
  <Exec WorkingDirectory="wwwroot\" Command="npm install --no-audit" />
  <Message Importance="high" Text="Done restoring dependencies using 'npm'." />
</Target>

On local machine, everything built correctly, node_modules downloaded, JavaScript and CSS loaded even with the versioning, the application was ready to be published into App Service (guess what, I hit another bump).

dotnet publish and node_modules

After I published the initial version to Azure App Service and loaded it, no CSS and JavaScript loaded. After further inspection in the filesystem through Kudu I noticed that there were no node_modules in the D;\home\site\wwwroot\wwwroot folder! Initially, I thought, hey, this must have been some bad luck, let's redeploy. I redeployed the application and indeed everything was fine.

Just to be sure, I decided to clean the wwwroot, built files in D:\home\site\repository and redeploy the application again (basically, we have a policy in our company, that the project has to build on a clean dev machine without any extra steps which then makes it super easy to put into a CI/CD pipeline, just a tip) and the same issue occured. After going through the generated build script (D:\home\site\deployment\tools\build.bat), I decided to run the steps manually.

First, dotnet restore which went fine and then dotnet publish with an output folder specified and Release configuration and whoops, the node_modules weren't part of my publish result either!

After spending some time on Google and GitHub issues (1, 2, 3), I found out what the issue was - it seems like the .NET SDK is precalculating the files for publishing but at that time, the node_modules folder hasn't been generated yet!

Then it was time to figure out a fix. I remembered, that the Single Page Application (SPA) templates for ASP.NET Core must work with NPM as well, and that those packages are definitely not included in Git (since the initial project files have about 300MB). So since we live in the opensource era, I found the repo with templates on GitHub and looked at one of the .csproj files. And there it was - the solution. Basically, through that .csproj, the publish process is made aware of the node_modules folder and includes it into the output.

Solving with NPM

So in the end it was all solved, I made few changes to the .csproj myself, so going to share those below, since I think they are useful:

  • When running the npm install command, run it with --no-audit parameter - this speeds up the build process.
  • On local build, always run npm install. Since multiple people work on the project, it is quite important that everyone has the appropriate packages. This added a slight overhead to the build time, but if all packages are in sync with package.json, it takes only about 0.1 seconds to finish which is kind of unnoticable.

The modifications to the .csproj which I am using are available below:

<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">
  <!-- Ensure Node.js is installed -->
  <Message Importance="high" Text="Checking Node.js presence..." />
  <Exec Command="node --version" ContinueOnError="true">
    <Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
  </Exec>
  <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
  <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
  <Exec WorkingDirectory="$(wwwroot)" Command="npm install --no-audit" />
  <Message Importance="high" Text="Done restoring dependencies using 'npm'." />
</Target>

<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
  <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
  <Exec WorkingDirectory="$(wwwroot)" Command="npm install --no-audit" />

  <!-- Include the newly-built files in the publish output -->
  <ItemGroup>
    <DistFiles Include="$(wwwroot)node_modules\**" />
    <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
      <RelativePath>%(DistFiles.Identity)</RelativePath>
      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
    </ResolvedFileToPublish>
  </ItemGroup>
</Target>

The real solution

Just a day before this post's release, I have learned (through the GitHub issue) about a project called Library Manager. It basically allows you to pull frontend libraries into your project - from cdnjs, local or network location. You simply specify a libman.json file, exclude the folder with libraries from Git and you are good to go. The build-time restore is also supported, simply by adding Microsoft.Web.LibraryManager.Build package to your project. An example libman.json file from the project is here:

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "defaultDestination": "wwwroot/lib",
  "libraries": [
    {
      "library": "jquery@3.2.1",
      "destination": "wwwroot/lib/jquery"
    },
    {
      "library": "twitter-bootstrap@3.3.7",
      "destination": "wwwroot/lib/bootstrap"
    },
    {
      "library": "jquery-validate@1.17.0",
      "destination": "wwwroot/lib/jquery-validate"
    },
    {
      "library": "jquery-validation-unobtrusive@3.2.10",
      "destination": "wwwroot/lib/jquery-validation-unobtrusive"
    }
  ]
}

Update (01SEP2018): Library Manager is now available in Visual Studio 15.8 release! There is also a handy CLI package so you can make use of it in your terminal outside Visual Studio.

Currently, there is an issue #66 which fails during build from CLI (and thus App Service as well). An easy fix is to edit the Microsoft.Web.LibraryManager.Build.targets file in your App Service directly (D:\home.nuget\microsoft.web.librarymanager.build\1.0.20\build) and you are good to go.

So go ahead and give it a try, it's easy as 1-2-3!

Comments

To submit comments, go to GitHub Discussions.