Extending SharePoint Framework Build Tasks

Applies To: SharePoint Framework

The SharePoint Framework (SPFx) uses gulp as the task runner that builds your solution. You can find out what tasks are available by typing gulp --tasks in the console in the root of your solution. But what if you want to extend these tasks?

There’s an excellent tutorial by Chaks Chandran that demonstrates how to do this: Integrate gulp tasks in SharePoint Framework toolchain. You can find the corresponding sample here: js-extend-gulp.

In this post, I’m going to demonstrate a few items that weren’t addressed in that tutorial including:

  • Checking for a custom parameter
  • Ending your task without a stream and not killing the pipeline
  • Getting a value from one of the Config files
  • Copying a solution file to another location

What Are We Building

I previously discussed how to specify your SharePoint Framework App Icon and in that post I noted that you should not only specify your icon in the solution package itself, but you should make it available outside as well so that you can set the App Catalog Icon URL property.

The best place to host this image is with the rest of your assets in your CDN. So, wouldn’t it be nice if the App Icon image file was copied to the dist folder when the rest of your solution was bundled? Let’s do it!

Basic Rigging

If your familiar with gulp you know that generally gulp tasks are defined in the gulpfile.js file in the root of your solution. More likely, however, you’re not familiar with gulp and don’t really know what a gulp task is for outside of the idea that they do things when you type them in the console.

A gulp task is just a JavaScript function that gets registered with a name (the command) and you can make it do whatever you want. In SPFx, however, all the gulp tasks are defined deep within the node_modules/@microsoft/gulp* folders. You don’t need to do anything with these (although they are worth checking out if your curious how they’re doing all this magic).

To jump in on the fun, you’ll need to use the build object and either rig your task into the pipeline or register it as a standalone task. Again, more detail can be found in the official tutorial. For our purposes, we don’t want a standalone task that we would have to call separately. We want our task to be executed along with the build.

Defining the Task

Open up your gulpfile.js file. Before the build.initialze(gulp) command add your sub task definition:

'use strict';

const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');

let appIconToBundle = build.subTask('app-icon-to-bundle', function(gulp, buildOptions, done) {
	this.log('Wowee!');
});

build.initialize(gulp);

In line 6, we’re simply calling the build.subTask function provided by Microsoft to register our function with the name app-icon-to-bundle (there’s nothing special about this name, just following the same over-hypenation as the default sub tasks).

This doesn’t actually do anything yet because we’ve neither registered it as a standalone task or integrated it into the build pipeline.

Rigging the Task to the Build

You can “rig” your task in 3 spots: PreBuild, PostTypescript, and PostBuild. You can think of these as specifying an event handler and the handler is your sub task. We’re going to add ours to the end of the build by adding line 10 below:

'use strict';

const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');

let appIconToBundle = build.subTask('app-icon-to-bundle', function(gulp, buildOptions, done) {
	this.log('Wowee!');
});

build.rig.addPostBuildTask(appIconToBundle);

build.initialize(gulp);

Now if we execute one of the building tasks (like “bundle” for instance) you’ll see our amazing task getting executed:

Basic Run

Properly Ending Your Task

Some of you may have noticed that there is a message about terminating early. In fact, if you run the gulp serve command you may be surprised to see it stops at the same spot and does none of the actual fanciness of serving your solution. WHAT DID YOU DO!?

The easiest way to fix this is by returning a stream and that’s exactly what is demonstrated in the official sample. But what about when you aren’t doing that? For our task we want to check some things and only proceed when it makes sense. This is hard to do in the context of a stream. The other option is to simply take advantage of the done callback parameter you are provided:

let appIconToBundle = build.subTask('app-icon-to-bundle', function(gulp, buildOptions, done) {
	this.log('Wowee!');
	done();
});

Now when you run the standard tasks you’ll still see our message but the pipeline doesn’t come to a screeching halt. Wowee, indeed.

Checking for a Custom Parameter

For our task, we only want to move the icon file if the --bundleicon parameter is specified. This is a custom parameter and detecting it is surprisingly easy:

let appIconToBundle = build.subTask('app-icon-to-bundle', function(gulp, buildOptions, done) {
	var bundleIcon = (process.argv.indexOf('--bundleicon') !== -1);
	this.log(bundleIcon ? 'Bundling!' : 'Nope!');
	done();
});

In line 7, we simply check for the presence of our custom parameter in the arguments and set a Boolean value. In line 8, we change the message based on the parameter’s inclusion. This could “easily” be extended to actually pull in values after your parameter.

To test this, simply run gulp bundle and then gulp bundle --bundleicon to see the 2 different messages. Our custom parameter works!

Getting a Value From One of the Config Files

Now we know when to actually move the icon based on the parameter, but how do we know where the icon actually is? The App Icon is an image file stored somewhere in the sharepoint folder. It is specified as the solution.iconPath property in the config/package-solution.json file.  More details here: SharePoint Framework App Icon

So, how do we get a value from one of the config files? First, we’ll need 2 helper objects. These objects are already included in your node_modules folder since they are used by the Microsoft build tasks. To gain access to them simply add some require statements below the existing require statements:

const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');

const path = require('path');
const fs = require('fs');

let appIconToBundle = build.subTask('app-icon-to-bundle', function(gulp, buildOptions, done) {
...

Specifically line 6 and 7 above add the path and fs objects which make it easy to work with file paths and reading files. Here’s how we use them:

let appIconToBundle = build.subTask('app-icon-to-bundle', function(gulp, buildOptions, done) {
	var bundleIcon = (process.argv.indexOf('--bundleicon') !== -1);

	if(bundleIcon){
		//Get the config file path
		var psConfigPath = path.join(process.cwd(), 'config', "package-solution.json");

		//read the config file into a JSON object
		var psConfig = undefined;
        try {
            var content = fs.readFileSync(psConfigPath, 'utf8');
            psConfig = JSON.parse(content);
        }
        catch (e) { }

		//Verify an iconPath has been provided
        if(psConfig && psConfig.solution && psConfig.solution.iconPath){
			this.log('./sharepoint/' + psConfig.solution.iconPath)
		}
	}

	done();
});

In line 14 we use the path object to get an absolute path to the package-solution.json config file. Then in line 19 we use the fs object to read the text of the config file. Since the config files are all JSON files, we can simply parse it into an object in line 20.

In line 25 we verify that the property was actually set and has a value. If so, we write it to the log in line 26. We are adding the ‘./sharepoint/’ because the iconPath property is relative to the sharepoint folder in the root of our project.

Copying the App Icon to the Dist Folder

Copying the file to the dist folder can be done using standard gulp:

        if(psConfig && psConfig.solution && psConfig.solution.iconPath){
			gulp.src('./sharepoint/' + psConfig.solution.iconPath)
				.pipe(gulp.dest('./dist'));
		}

The Final Solution

Here’s the final gulpfile.js:

'use strict';

const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');

const path = require('path');
const fs = require('fs');

//When the --bundleicon parameter is used, copies the App Icon image file (when specified) to the dist folder
let appIconToBundle = build.subTask('app-icon-to-bundle', function(gulp, buildOptions, done) {
	//Look for the --bundleicon parameter
	var bundleIcon = (process.argv.indexOf('--bundleicon') !== -1);

	if(bundleIcon){
		//Get the config file path
		var psConfigPath = path.join(process.cwd(), 'config', "package-solution.json");

		//read the config file into a JSON object
		var psConfig = undefined;
        try {
            var content = fs.readFileSync(psConfigPath, 'utf8');
            psConfig = JSON.parse(content);
        }
        catch (e) { }

		//Verify an iconPath has been provided
        if(psConfig && psConfig.solution && psConfig.solution.iconPath){
			//Copy the icon to the assets destination
			gulp.src('./sharepoint/' + psConfig.solution.iconPath)
				.pipe(gulp.dest('./dist'));
		}
	}

	done();
});

build.rig.addPostBuildTask(appIconToBundle);

build.initialize(gulp);

You can find the full solution here: https://github.com/thechriskent/spfx-AppIcon

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s