SharePoint Framework On-Premise Deploy via Gulp

SharePoint 2016 Server has support for SharePoint Framework, however there are some features missing that are present in SharePoint Online.

When first learning SharePoint Framework (SPFx), the tutorials provided by Microsoft are geared towards using SPFx in SharePoint Online.  A lot of the knowledge and concepts transfer to SharePoint 2016 Server, however I found a key feature missing: Asset deployment.

SPFx solutions expect you to host your assets (html, javascript, css) in some location that is accessible from your SharePoint site.  In SharePoint Online, the deployment gulp task that comes packaged with SPFx can (and by default, does) deploy your assets to the SharePoint Online App Catalog site.  With SharePoint 2016 Server, asset deployment is your responsibility.  I did not like this at all – I wanted a simple, configurable script/task to run that would deploy my assets where I wanted them to go.

First the setup. 

I ended up deciding to mimic the deployment method used by SharePoint Online SPFx solutions.  With SharePoint Online, assets are deployed into a document library in the App Catalog called “ClientSideAssets”, in a folder named using the GUID for the SPFx solution.  For SharePoint 2016 Server, you must create an App Catalog site to deploy your solution to, but the “ClientSideAssets” document library does not exist by default.  So I created it to upload my assets into, making sure the permissions were set such that anyone would be able to read from the document library.

After manually uploading the compiled assets into a manually created folder in the “ClientSideAssets” document library and pointing my solution at those assets to verify everything was working correctly, it was time to write the series of gulp tasks to automate this process and mimic what the SharePoint Online deployment task does.

The general outline of what needed to be done was as follows:

  1. Update the config/write-manifests.json file to point to the asset location so the SPFx solution knows where the assets are going to be uploaded to (and consequently, fetched from).
  2. Bundle and Package the solution for production (this step is just the “gulp bundle –ship” and “gulp package-solution –ship” tasks Microsoft’s tutorial goes over).
  3. Deploy the assets (involving deleting the existing assets if there are any) to the proper folder in SharePoint 2016 App Catalog
  4. Upload the SPFx solution to the SharePoint 2016 App Catalog

Simple enough.  A few packages are needed from NPM to assist with these steps.

  • gulp-spsave – used to upload files to SharePoint
  • sppurge – used to delete files from SharePoint
  • run-sequence – for easily running gulp tasks sequentially

In addition to those packages, the modified SPFx gulp file will also need to use the file system package (“fs”) and to import the package-solution.json and copy-assets.json files from the config directory of the SPFx solution (so we can use data from those files), as well as 2 custom settings .json files that will be created.  These .json files can be created anywhere, but I just put them at the root of my project:

gulp-settings.json

{
  "sharePointAppCatalogSiteUrl": "http://myspsite/sites/apps",
  "sharePointAppCatalogClientSideAssetsLibrary": "ClientSideAssets"
}

gulp-settings-security.json

{
  "sharePointCredentials": {
    "username": "username",
    "password": "password",
    "domain": "domain"
  }
}

The idea is that the gulp-settings.json file can be checked into source control as-is, but since the gulp-setings-security.json file has sensitive information in it, it should be initially checked into source control as a template but then ignored.

package-solution.json provides us the GUID of the SPFx solution while copy-assets.json lets us know where on the file system the assets get created.

Now to actually create the gulp tasks.  Following the general outline, the first step is to update the SPFx write-manifests.json file so the solution knows where the assets will be located.

gulp.task("update-write-manifests", function() {
  const cdnBasePath = `${gulpSettings.sharePointAppCatalogSiteUrl}/${gulpSettings.sharePointAppCatalogClientSideAssetsLibrary}/${packageSolutionSettings.solution.id}`;
  const writeManifestsFileLoc = "./config/write-manifests.json";
  var writeManifestsFile = fs.readFileSync(writeManifestsFileLoc).toString();
  writeManifestsFile = writeManifestsFile.replace(/\"cdnBasePath\": \".*\"/gi, `"cdnBasePath": "${cdnBasePath}"`);
  fs.writeFileSync(writeManifestsFileLoc, writeManifestsFile);
});

This task compiles the url path for where the assets are going to go, then updates the write-manifests.json file with that path.

Step 2 of the outline is already handled by built-in gulp tasks from Microsoft, so deploying the compiled assets is next.  This involves first deleting any existing assets from SharePoint, then uploading the just-compiled assets.

gulp.task("delete-assets", function() {
  const context = {
    siteUrl: gulpSettings.sharePointAppCatalogSiteUrl,
    creds: {
      username: gulpSettingsSecurity.sharePointCredentials.username,
      password: gulpSettingsSecurity.sharePointCredentials.password,
      domain: gulpSettingsSecurity.sharePointCredentials.doman
    }
  };
  const options = {
    folder: `${gulpSettings.sharePointAppCatalogClientSideAssetsLibrary}/${packageSolutionSettings.solution.id}`,
    fileRegExp: new RegExp(".*\..*", "i")
  };

  return sppurge(context, options);
});

gulp.task("upload-assets", function() {
  const coreOptions = {
    siteUrl: gulpSettings.sharePointAppCatalogSiteUrl,
    folder: `${gulpSettings.sharePointAppCatalogClientSideAssetsLibrary}/${packageSolutionSettings.solution.id}`
  };
  const creds = {
    username: gulpSettingsSecurity.sharePointCredentials.username,
    password: gulpSettingsSecurity.sharePointCredentials.password,
    domain: gulpSettingsSecurity.sharePointCredentials.doman
  };
  return gulp.src(`${copyAssetsSettings.deployCdnPath}/*.js`).pipe(spsave(coreOptions, creds));
});

SharePoint Framework compiles into a set of .json and .js files.  The only ones that are needed are the .js files, so the upload-assets task uses a file mask to only upload those files to our assets location.

Finally, we’ll upload the SPFx solution to the App Catalog.  Note that the first time this solution is uploaded to the App Catalog, it will need to be “Deployed”, which requires manual interaction.  All subsequent times do not require this.

gulp.task("upload-solution", function() {
  const coreOptions = {
    siteUrl: gulpSettings.sharePointAppCatalogSiteUrl,
    folder: "AppCatalog"
  };
  const creds = {
    username: gulpSettingsSecurity.sharePointCredentials.username,
    password: gulpSettingsSecurity.sharePointCredentials.password,
    domain: gulpSettingsSecurity.sharePointCredentials.doman
  };
  return gulp.src(`sharepoint/${packageSolutionSettings.paths.zippedPackage}`).pipe(spsave(coreOptions, creds));
});

The resulting gulp file looks like this:

'use strict';

const gulp = require("gulp");
const build = require("@microsoft/sp-build-web");
const spsave = require("gulp-spsave");
const sppurge = require("sppurge").default;
const runSequence = require("run-sequence");
const fs = require('fs');
const gulpSettings = require("./gulp-settings.json");
const gulpSettingsSecurity = require("./gulp-settings-security.json");
const packageSolutionSettings = require("./config/package-solution.json");
const copyAssetsSettings = require("./config/copy-assets.json");

build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);

gulp.task("delete-assets", function() {
  const context = {
    siteUrl: gulpSettings.sharePointAppCatalogSiteUrl,
    creds: {
      username: gulpSettingsSecurity.sharePointCredentials.username,
      password: gulpSettingsSecurity.sharePointCredentials.password,
      domain: gulpSettingsSecurity.sharePointCredentials.doman
    }
  };
  const options = {
    folder: `${gulpSettings.sharePointAppCatalogClientSideAssetsLibrary}/${packageSolutionSettings.solution.id}`,
    fileRegExp: new RegExp(".*\..*", "i")
  };

  return sppurge(context, options);
});

gulp.task("upload-assets", function() {
  const coreOptions = {
    siteUrl: gulpSettings.sharePointAppCatalogSiteUrl,
    folder: `${gulpSettings.sharePointAppCatalogClientSideAssetsLibrary}/${packageSolutionSettings.solution.id}`
  };
  const creds = {
    username: gulpSettingsSecurity.sharePointCredentials.username,
    password: gulpSettingsSecurity.sharePointCredentials.password,
    domain: gulpSettingsSecurity.sharePointCredentials.doman
  };
  return gulp.src(`${copyAssetsSettings.deployCdnPath}/*.js`).pipe(spsave(coreOptions, creds));
});

gulp.task("deploy-assets", function() {
  runSequence("delete-assets", "upload-assets");
});

gulp.task("upload-solution", function() {
  const coreOptions = {
    siteUrl: gulpSettings.sharePointAppCatalogSiteUrl,
    folder: "AppCatalog"
  };
  const creds = {
    username: gulpSettingsSecurity.sharePointCredentials.username,
    password: gulpSettingsSecurity.sharePointCredentials.password,
    domain: gulpSettingsSecurity.sharePointCredentials.doman
  };
  return gulp.src(`sharepoint/${packageSolutionSettings.paths.zippedPackage}`).pipe(spsave(coreOptions, creds));
});

gulp.task("update-write-manifests", function() {
  const cdnBasePath = `${gulpSettings.sharePointAppCatalogSiteUrl}/${gulpSettings.sharePointAppCatalogClientSideAssetsLibrary}/${packageSolutionSettings.solution.id}`;
  const writeManifestsFileLoc = "./config/write-manifests.json";
  var writeManifestsFile = fs.readFileSync(writeManifestsFileLoc).toString();
  writeManifestsFile = writeManifestsFile.replace(/\"cdnBasePath\": \".*\"/gi, `"cdnBasePath": "${cdnBasePath}"`);
  fs.writeFileSync(writeManifestsFileLoc, writeManifestsFile);
});

gulp.task("prep-for-deploy", function() {
  runSequence("clean", "update-write-manifests");
});

gulp.task("deploy-assets-and-solution", function() {
  runSequence("deploy-assets", "upload-solution");
});

build.initialize(gulp);

Finally, to make this entire process runable via just one command, I modified the package.json file to add some scripts:

"scripts": {
  "build": "gulp bundle",
  "clean": "gulp clean",
  "test": "gulp test",
  "bundle-ship": "gulp bundle --ship || echo \"gulp bundle errored\"",
  "package-solution-ship": "gulp package-solution --ship || echo \"gulp package-solution errored\"",
  "deploy": "gulp prep-for-deploy && npm run bundle-ship && npm run package-solution-ship && gulp deploy-assets-and-solution"
}

Notice that the Microsoft-provided gulp tasks of “bundle” and “package-solution” are being called via some custom scripts utilizing an “or” operator.  The reason for this is two-fold:

  1. You can’t trigger a gulp task with switch/flag arguments from within another gulp task.  I blame Microsoft for not providing a separate gulp task, instead of re-using the gulp tasks but utilizing a flag to decide if bunde/package-solution should be built for production.
  2. The “or” operator that echoes a message is a workaround.  If you don’t do it this way and there is ANYTHING wrong with your bundle/package-ship task, including just warnings, the “deploy” script will quit prematurely.  The “or” operator causes the output to ignore warning statements (basically anything that isn’t a critical error).

“npm run deploy” will now update the proper .json files, compile and deploy your SPFx solution!

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 )

Google+ photo

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

Twitter picture

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

Facebook photo

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

Connecting to %s