SPFx 1.22 with Heft — Sync solution version from package.json

SPFx 1.22 with Heft — Sync solution version from package.json

Why sync versions

Keeping the SharePoint package solution version aligned with your package.json version reduces confusion across deployments and makes release tracking consistent.

With SPFx 1.22, the build toolchain moved to Heft. This enables clean pre-/post-task customizations without touching legacy gulp tasks.

References:

Approach

We’ll read package.json.version and update sharepoint/solution/*.json (.sppkg manifest source) before package-solution runs, using the Heft Run Script plugin.

1) Add project config/heft.json

Create config/heft.json that extends the SPFx rig and adds a task into the build phase.

{
  "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json",
  "extends": "@microsoft/spfx-web-build-rig/profiles/default/config/heft.json",
  "phasesByName": {
    "build": {
      "tasksByName": {
        "sync-solution-version": {
          "taskPlugin": {
            "pluginPackage": "@rushstack/heft",
            "pluginName": "run-script-plugin",
            "options": {
              "scriptPath": "./config/run-script/sync-solution-version.mjs"
            }
          }
        }
      }
    }
  }
}

2) Add the script config/run-script/sync-solution-version.mjs

This script reads package.json and updates the solution manifest JSON (for example ./config/package-solution.json or ./sharepoint/solution/package-solution.json, depending on your project layout).

import fs from "node:fs";
import path from "node:path";

export async function runAsync() {
  const root = process.cwd();
  const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
  const version = pkg.version;

  // Common locations; adjust to your project
  const candidates = [
    path.join(root, "config", "package-solution.json"),
    path.join(root, "sharepoint", "solution", "package-solution.json")
  ];

  let updated = false;
  for (const file of candidates) {
    if (fs.existsSync(file)) {
      const json = JSON.parse(fs.readFileSync(file, "utf8"));
      if (json.solution && json.solution.version) {
        json.solution.version = coerceSpfxVersion(version);
        fs.writeFileSync(file, JSON.stringify(json, null, 2));
        console.log(`[sync-version] Updated ${file} -> ${json.solution.version}`);
        updated = true;
      }
    }
  }

  if (!updated) {
    console.warn("[sync-version] No package-solution.json found to update.");
  }
}

function coerceSpfxVersion(v) {
  // SPFx expects 4-part semver (major.minor.patch.build). Map npm semver to 4 parts.
  // 1.2.3 => 1.2.3.0; 1.2.3-beta => 1.2.3.0
  const m = /^([0-9]+)\.([0-9]+)\.([0-9]+)/.exec(v);
  if (!m) return "1.0.0.0";
  const [_, major, minor, patch] = m;
  return `${major}.${minor}.${patch}.0`;
}

3) Use the script via npm

Add an npm script to trigger Heft packaging (SPFx 1.22 ships these by default, but add if missing):

{
  "scripts": {
    "build": "heft build"
  }
}

Now when you run npm run build, the sync-solution-version task executes in the build phase and updates the solution manifest version from package.json.

Sample output

[build:set-browserslist-ignore-old-data-env-var] Setting environment variable BROWSERSLIST_IGNORE_OLD_DATA=1
[sync-version] Updated ../session_samples/heft/config/package-solution.json -> 0.0.1.0

Notes

  • If you maintain multiple solutions, iterate all manifests in the script.
  • For advanced scenarios, replace Run Script with a dedicated Heft plugin.

Verified docs


TL;DR

  • Add config/heft.json and a run-script to set solution.version.
  • Map npm semver to SPFx 4-part version.
  • Run heft build to keep versions aligned.