In this post, I will describe how to use Gulp tasks to automate the compiling of SASS files and the bundling and minifying of CSS and JS files inside a Razor class library (RCL). The goal is to run the tasks on every build using the dotnet build or when building using Visual Studio to make it as seamless as possible.

For our example we will be compiling a SASS file site.scss and bundling a couple of JS files site.js, lib-a.js and lib-b.js. These files are located in the wwwroot folder of our RCL project.

The folder structure looks like this:

wwwroot
    | theme
        | css
        | js
        - site.scss
        - site.js
        - lib-a.js
        - lib-b.js

The root of the theme folder contains the files we need to compile, bundle and minify. And the css and js folders will contain the resulting files.

Gulp tasks

I won't go into much detail on the actual Gulp tasks, because that is a whole other topic. But the following is the basic setup needed for our example.

The package.json used to install the dependencies needed for the tasks.

{
  "private": true,
  "devDependencies": {
    "gulp": "3.9.1",
    "gulp-concat": "2.6.1",
    "gulp-cssmin": "0.2.0",
    "gulp-uglify": "3.0.2",
    "rimraf": "2.6.3",
    "gulp-sass": "4.0.2",
    "run-sequence": "2.2.1"
  }
}

The gulpfile.js with the tasks to compile the SASS (sass) and a task for bundling and minifying the CSS and JS (min). And a task to clean the folders (clean).

var gulp = require('gulp'),
    rimraf = require('rimraf'),
    concat = require('gulp-concat'),
    cssmin = require('gulp-cssmin'),
    uglify = require('gulp-uglify'),
    sass = require('gulp-sass'),
    runSequence = require('run-sequence');

var paths = {
    root: './wwwroot/theme/'
};

paths.js = paths.root + '*.js';
paths.minJs = paths.root + 'js/*.min.js';
paths.css = paths.root + 'css/*.css';
paths.minCss = paths.root + 'css/*.min.css';
paths.concatJsDest = paths.root + 'js/site.min.js';
paths.concatCssDest = paths.root + 'css/site.min.css';

gulp.task('default', function (done) {
    runSequence('clean', 'sass', 'min', function () { done(); });
});

gulp.task('clean:js', function (cb) {
    rimraf(paths.concatJsDest, cb);
});
gulp.task('clean:css', function (cb) {
    rimraf(paths.concatCssDest, cb);
});
gulp.task('clean', ['clean:js', 'clean:css']);

gulp.task('sass', function () {
    return gulp.src(paths.root + '/site.scss')
        .pipe(sass())
        .pipe(gulp.dest(paths.root + '/css'));
});

gulp.task('min:js', function () {
    return gulp.src([paths.js, '!' + paths.minJs], { base: '.' })
        .pipe(concat(paths.concatJsDest))
        .pipe(uglify())
        .pipe(gulp.dest('.'));
});
gulp.task('min:css', function () {
    return gulp.src([paths.css, '!' + paths.minCss])
        .pipe(concat(paths.concatCssDest))
        .pipe(cssmin())
        .pipe(gulp.dest('.'));
});
gulp.task('min', ['min:js', 'min:css']);

Execute the tasks from a project file

To execute the Gulp task during the build process we need add some build tasks to the *.csproj file.

To execute a task in a project file, create an Exec element with the command for the task as a child of a Target element. And specify that this target should run before the Build target.

<Target Name="MyPreCompileTarget" BeforeTargets="Build"> ... </Target>

Ensure Node.js is installed

Because Gulp depends on Node.js we need to ensure that it is installed before we run the tasks. We can do that by adding a command that checks the installed version of node, and if that command fails we throw an error to indicate that it needs to be installed.

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

Building the project without having Node.js installed will now give you a nice error message.

11>..\MyRazorUI\MyRazorUI.csproj(18,9): error : 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.

Restoring dependencies using NPM

If Node.js is installed we can restore the dependencies needed to run the Gulp task.

<Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
<Exec WorkingDirectory="$(ProjectDir)" Command="npm install" />

Run the Gulp tasks

When everything has been installed en restored, we can run the tasks to compile the SASS files and bundle and minify the CSS and JS files.

<Exec WorkingDirectory="$(ProjectDir)" Command="node_modules\.bin\gulp default" />

The completed Target element looks like this:

<Target Name="MyPreCompileTarget" BeforeTargets="Build">
    <!-- Ensure Node.js is installed -->
    <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="$(ProjectDir)" Command="npm install" />
    <Exec WorkingDirectory="$(ProjectDir)" Command="node_modules\.bin\gulp default" />
</Target>

Include the bundled and minified files in a Razor class library (RCL)

What is a Razor class library (RCL)

Razor views, pages, controllers, page models, Razor components, View components, and data models can be built into a RCL. This RCL can be packaged and reused.

See Razor class libraries for more information. Razor class libraries requires .NET Core 2.1 SDK or later.

Embedding static files in a Razor class library

We want our bundled and minified files also to be packaged in our RCL. But Razor class libraries by default can not expose static files. For this you need to embed your static assets into your RCL assembly and add a FileProvider to serve your static files.

For embedding the files we can use a wildcard include to include multiple files at once. And we don't need (or want) to package the sources, so we won't include them.

<ItemGroup>
    <EmbeddedResource Include="wwwroot\**\css\*" />
    <EmbeddedResource Include="wwwroot\**\js\*" />
</ItemGroup>

Serving static files

To actually be able to serve the embedded files in our application we need to create an additional FileProvider in our RCL, pointing to the resources folder, and adds it to those that retrieve static files. This will allow you to reference the files in your HTML like any other static file.

public class StaticFilePostConfigureOptions : IPostConfigureOptions<StaticFileOptions>
{
    private readonly IHostingEnvironment _environment;

    public StaticFilePostConfigureOptions(IHostingEnvironment environment)
    {
        _environment = environment;
    }

    public void PostConfigure(string name, StaticFileOptions options)
    {
        options.ContentTypeProvider = options.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
        if (options.FileProvider == null && _environment.WebRootFileProvider == null)
            throw new InvalidOperationException("Missing FileProvider.");

        options.FileProvider = options.FileProvider ?? _environment.WebRootFileProvider;

        var filesProvider = new ManifestEmbeddedFileProvider(GetType().Assembly, "wwwroot");
        options.FileProvider = new CompositeFileProvider(options.FileProvider, filesProvider);
    }
}

Make sure we configure the FileProvider in the dependency injection container of our application.

services.ConfigureOptions(typeof(StaticFilePostConfigureOptions));

And because the FileProvider needs manifest of the embedded files, we need to generate that manifest too. For this we set the GenerateEmbeddedFilesManifest property in the project file to true.

<PropertyGroup>
    ...
    <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
    ...
</PropertyGroup>

Using the compiled, bundled and minified CSS and JS files

Now we can now add a reference to the embedded files in HTML files in any web project that references the RCL.

<script src="~/theme/css/app.bundle.js" asp-append-version="true"></script>
<script src="~/theme/js/site.min.js" asp-append-version="true"></script>

You can see an implementation in Opw.PineBlog.RazorPages. github.com/ofpinewood/pineblog/tree/master/src/Opw.PineBlog.RazorPages