Package SPFx solution with one command and automatically increase the version

Package SPFx solution with one command and automatically increase the version

In SPFx solutions, the version number is stored in the package-solution.json file. The versioning follows the scheme used in assemblies, for example. For example, 1.0.0.0 (Major.Minor.Build.Revision).

Most people (and I have to admit that I am one of them) almost never incremented this number. At least the packages did not have to be "updated" manually and received all changes immediately. You also have to remember to update the version number. It is, of course, clear that this is not the purpose of versioning. But recently, Microsoft has published an important update, which obliges to update the version number. Because if you do not updates the version, the solution changes will not be visible. So you have two options:

  1. Uninstall solution, delete solution from the Recycle Bin, delete solution from the 2nd Stage Recycle Bin and then reinstall.

  2. Update the version number and then perform an "update" for the app.

I think it is clear which is faster/easier. But the problem is that you must not forget to update the version. I came across the "automatic version increment" on a blog a few years ago, which uses a gulp task. But - as described above - I didn't see the point in incrementing the version because the other thing is easier. Upload solution - done. No update, no waiting. It is simple and fast. With the Microsoft Update, such a task makes sense, of course. So I went looking for this script again and came across another - but still useful - blog article by Tom Daly. I found his code quite good and wanted to adapt it to my needs.

For my SPFx solutions that I publish on GitHub, I include the version number in the web parts. This has become established for many WebParts. For this purpose, I usually use the PnP Property Pane Controls and the control PropertyPaneWebPartInformation. To display the version number, you can access the Web Part context object this.context.manifest.version. The problem is that this is not the version number from package-solutions.json, but the one from package.json. This is needed for npm. But npm, like many others, uses the Semantic Versioning. This version has 3 digits. So 1.0.0 (Major.Minor.Patch).

My goal was to automatically update both, the package-solution.json and the package.json and to keep them "in sync". The revision number of the package-solution.json is completely ignored. The version number should always be incremented automatically when the gulp bundle command is executed with the parameter --ship. Alternatively, you can just update the version with the command gulp bump-version. You can specify which part of the versioning should be updated with the additional parameters --major, --minor or --patch. Whereas --patch is the default. If you don't want to update the version in the gulp bundle --ship command, you can just type gulp bundle --ship --no-patch and then nothing will be done with the version.

To use my script, you need to add the following lines to the top of gulpfile.js (if not already there):

const gulp = require('gulp'); 
const gutil = require('gulp-util'); 
const fs = require('fs');

Then replace the last line build.initialize(require('gulp')); with build.initialize(gulp);. And before this line, insert the following code:

var getJson = function (file) {
  return JSON.parse(fs.readFileSync(file, 'utf8'));
};

let bumpVersionSubTask = build.subTask('bump-version-subtask', function(gulp, buildOptions, done) {

  const currentCommand = buildOptions.args._[0];

  const skipFunc = gulp.src('./config/package-solution.json').pipe(gutil.noop());

  if(typeof currentCommand != "string") {
    gutil.log("The current command is undefined, skip version bump");
    return skipFunc;
  }

  const commandName = currentCommand.toLocaleLowerCase();

  if(commandName != "bundle" && commandName != "bump-version") {
    gutil.log("The current command is not 'bundle' or 'bump-version', skip version bump");
    return skipFunc;
  }

  const bumpVersion = commandName == "bump-version" || buildOptions.args["ship"] === true;

  if(!bumpVersion) {
      gutil.log("The current command is not 'bump-version' or the --ship argument was not specified, skip version bump");
      return skipFunc;
  }

  const a = buildOptions.args;

  const skipMajorVersion = typeof a["major"] == "undefined" || a["major"] === false;
  const skipMinorVersion = !skipMajorVersion || typeof a["minor"] == "undefined" || a["minor"] === false;
  const skipPatchVersion = !skipMajorVersion || !skipMinorVersion || a["patch"] === false;

  if(skipMajorVersion && skipMinorVersion && skipPatchVersion) {
    gutil.log("skip version bump, because all specified arguments (major, minor, patch) are set to 'false'")
    return skipFunc;
  }

  const pkgSolutionJson = getJson('./config/package-solution.json');
  const currentVersionNumber = String(pkgSolutionJson.solution.version);
  let nextVersionNumber = currentVersionNumber.slice();
  let nextVersionSplitted = nextVersionNumber.split('.');
  gutil.log('Current version: ' + currentVersionNumber);

  if(!skipMajorVersion) {
    nextVersionSplitted[0] = parseInt(nextVersionSplitted[0]) + 1;
    nextVersionSplitted[1] = 0;
    nextVersionSplitted[2] = 0;
    nextVersionSplitted[3] = 0;
  }

  if(!skipMinorVersion) {
    nextVersionSplitted[1] = parseInt(nextVersionSplitted[1]) + 1;
    nextVersionSplitted[2] = 0;
    nextVersionSplitted[3] = 0;
  }

  if(!skipPatchVersion) {
    nextVersionSplitted[2] = parseInt(nextVersionSplitted[2]) + 1;
    nextVersionSplitted[3] = 0;
  }

  nextVersionNumber = nextVersionSplitted.join(".");

  gutil.log('New version: ', nextVersionNumber);

  pkgSolutionJson.solution.version = nextVersionNumber;
  fs.writeFile('./config/package-solution.json', JSON.stringify(pkgSolutionJson, null, 4), () => {});

  const packageJson = getJson('./package.json');
  packageJson.version = nextVersionNumber.split('.').splice(0, 3).join(".");
  fs.writeFile('./package.json', JSON.stringify(packageJson, null, 4), () => {});

  return gulp.src('./config/package-solution.json')
  .pipe(gulp.dest('./config'));
});

let bumpVersionTask = build.task('bump-version', bumpVersionSubTask);
build.rig.addPreBuildTask(bumpVersionTask);

The complete "gulpfile.js" looks like this:

'use strict';

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

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

var getTasks = build.rig.getTasks;
build.rig.getTasks = function () {
  var result = getTasks.call(build.rig);

  result.set('serve', result.get('serve-deprecated'));

  return result;
};

var getJson = function (file) {
  return JSON.parse(fs.readFileSync(file, 'utf8'));
};

let bumpVersionSubTask = build.subTask('bump-version-subtask', function(gulp, buildOptions, done) {

  const currentCommand = buildOptions.args._[0];

  const skipFunc = gulp.src('./config/package-solution.json').pipe(gutil.noop());

  if(typeof currentCommand != "string") {
    gutil.log("The current command is undefined, skip version bump");
    return skipFunc;
  }

  const commandName = currentCommand.toLocaleLowerCase();

  if(commandName != "bundle" && commandName != "bump-version") {
    gutil.log("The current command is not 'bundle' or 'bump-version', skip version bump");
    return skipFunc;
  }

  const bumpVersion = commandName == "bump-version" || buildOptions.args["ship"] === true;

  if(!bumpVersion) {
      gutil.log("The current command is not 'bump-version' or the --ship argument was not specified, skip version bump");
      return skipFunc;
  }

  const a = buildOptions.args;

  const skipMajorVersion = typeof a["major"] == "undefined" || a["major"] === false;
  const skipMinorVersion = !skipMajorVersion || typeof a["minor"] == "undefined" || a["minor"] === false;
  const skipPatchVersion = !skipMajorVersion || !skipMinorVersion || a["patch"] === false;

  if(skipMajorVersion && skipMinorVersion && skipPatchVersion) {
    gutil.log("skip version bump, because all specified arguments (major, minor, patch) are set to 'false'")
    return skipFunc;
  }

  const pkgSolutionJson = getJson('./config/package-solution.json');
  const currentVersionNumber = String(pkgSolutionJson.solution.version);
  let nextVersionNumber = currentVersionNumber.slice();
  let nextVersionSplitted = nextVersionNumber.split('.');
  gutil.log('Current version: ' + currentVersionNumber);

 if(!skipMajorVersion) {
    nextVersionSplitted[0] = parseInt(nextVersionSplitted[0]) + 1;
    nextVersionSplitted[1] = 0;
    nextVersionSplitted[2] = 0;
    nextVersionSplitted[3] = 0;
  }

  if(!skipMinorVersion) {
    nextVersionSplitted[1] = parseInt(nextVersionSplitted[1]) + 1;
    nextVersionSplitted[2] = 0;
    nextVersionSplitted[3] = 0;
  }

  if(!skipPatchVersion) {
    nextVersionSplitted[2] = parseInt(nextVersionSplitted[2]) + 1;
    nextVersionSplitted[3] = 0;
  }

  nextVersionNumber = nextVersionSplitted.join(".");

  gutil.log('New version: ', nextVersionNumber);

  pkgSolutionJson.solution.version = nextVersionNumber;
  fs.writeFile('./config/package-solution.json', JSON.stringify(pkgSolutionJson, null, 4), () => {});

  const packageJson = getJson('./package.json');
  packageJson.version = nextVersionNumber.split('.').splice(0, 3).join(".");
  fs.writeFile('./package.json', JSON.stringify(packageJson, null, 4), () => {});

  return gulp.src('./config/package-solution.json')
  .pipe(gulp.dest('./config'));
});

let bumpVersionTask = build.task('bump-version', bumpVersionSubTask);
build.rig.addPreBuildTask(bumpVersionTask);

build.initialize(gulp);

Now you can use the task gulp bundle --ship or gulp bump-version to automatically bump up the version.

One command for complete release including version update

To publish the package, you normally have to execute the following commands:

gulp clean

gulp build

gulp bundle --ship

gulp package-solution --ship

Of course, you can also use the shorthand notation gulp clean; gulp build; gulp bundle --ship; gulp package-solution --ship. But this also takes a long time. That's why I show you here how you can do everything at once with one command. Including version updates. I have thought of the command publish for this. You can simply use the npm scripts for this. Open the package.json file and enter the following under the scripts property:

"publish": "gulp clean && gulp build && gulp bundle --ship",
"postpublish": "gulp package-solution --ship"

package.json file example

Now you can just run npm run publish in the terminal and you have everything in one command. Even the version is incremented because of our previously described gulp task.

By the way, if you don't want the version to be incremented, just run the command like this npm run publish -- --no-patch. If the major version is to be updated, then just use npm run publish -- --major. For minor then npm run publish -- --minor.

I hope it has helped you and that you - like me - have saved some time.

Happy coding ;-)

Did you find this article valuable?

Support $€®¥09@ by becoming a sponsor. Any amount is appreciated!