npm Scripts for SPFx: Stop Memorizing Heft Flags

Now that SharePoint Framework (SPFx) 1.22 includes the Heft toolchain, let’s talk about npm scripts. This method has been supported since SPFx 1.0 and warrants more attention.

The goal isn’t to replace Heft, but to improve it by eliminating command-line flags. Those Heft options get cumbersome fast. Why should I memorize them or look them up every time?

SharePoint Framework already comes with the following npm scripts out of the box.

{
  "scripts": {
    "build": "heft test --clean --production && heft package-solution --production",
    "start": "heft start --clean",
    "clean": "heft clean",
    "eject-webpack": "heft eject-webpack"
  }
}

SPFx does not own them. They are part of your project setup and yours to customize.

Don’t make me think

I experience significant pain when a new tool emerges, and I am told to relearn everything I already know. That is how it felt when Heft became the default driver for SharePoint projects.

A better way is to define requirements once and turn them into a reusable pattern for every new project. Switching between pure Node.js, Storybook, Vite, and Pattern Lab should not require remembering thousands of command-line options, switches, and knobs.

Give me one thing that all projects share, and npm scripts are exactly that. Configure them once, have them documented automatically, and keep them easy to adjust for each project, not only for SharePoint Framework based on Heft.

Naming convention

Naming conventions for npm scripts let me summarize things under a common banner while still giving me options.

A widely adopted pattern in the NodeJS development community is sometimes called npm script namespacing or colon notation.

The pattern for an npm script goes like this:

<command>:<variant>

Common naming patterns:

PatternExamples
cmd:environmentbuild:dev, build:prod, build:staging
cmd:scopetest:unit, test:e2e, test:integration
cmd:modifierstart:clean, start:debug, start:watch
cmd:platformbuild:web, build:mobile, build:electron

Looking at the build scripts above, Heft first runs --clean to remove previous builds, then tests in --production, and creates a packaged solution for production. A bit hard to read, and I sometimes don’t want to build it for production. I may like to use it on my dev tenant for development.

A problem easy to solve with that naming convention.

{
  "scripts": {
    "package": "npm run clean && heft package-solution ",
    "package:prod": "npm run clean && heft test --production && heft package-solution --production",
    "clean": "heft clean"
  },
}

This adds two new options to the package command. The default behaviour is to run npm run package to create a development build; package:prod instead produces a full production build.

In a CI/CD pipeline, I can simplify this to run npm run package:prod. If something changes in the future, the only thing that needs to be done is to update my project configuration, not the pipeline.

In addition to the clean script, another npm script is reused.

Reusing Scripts (DRY – Don’t repeat yourself)

Notice how npm run clean script, using heft clean, appears inside other scripts:

{
  "scripts": {
    "package": "npm run clean && heft package-solution ",
    "package:prod": "npm run clean && heft test --production && heft package-solution --production",
    "clean": "heft clean"
  },
}

If the heft clean command ever changes, you only need to update it once. All dependent scripts automatically use the new version.

We have already done that with gulp clean.

Documentation

With a configuration like this, you have a clear order in your package.json and create easy-to-read sections. With a simple trick, you can create section headlines as well, which provide immediate documentation for the section. Here is one example.

{
  "scripts": {
      "📦 Packaging": "Package the SPFx project for dev and :prod for production",
  }
}

Give it a name, and, if you like, an emoji and a description.

Technically, it is an npm script, but the package.json file clearly defines sections.

{
  "scripts": {
    "📦 Packaging": "Package the SPFx project for dev and prod for production",
    "package": "npm run clean && heft package-solution ",
    "package:prod": "npm run clean && heft test --production && heft package-solution --production",

    "👩🏻‍💻 Development": "When you update your code",
    "dev": "heft start --clean",
    "dev:nobrowser": "heft start --clean --nobrowser",
    "start": "heft start --clean",

    "🧰 Utilities": "Common Utilities",
    "clean": "heft clean",
    "webpack:eject": "heft eject-webpack"
  },
}

How can you benefit from this? When you just run npm run, you get a clear list of options, visually highlighted.

npm script options outputted on the terminal
Npm run shows all the available options for you ready to use

A clear separation of concerns.

NPM Scripts and Hooks

NPM comes with a couple of predefined script options, or reserved lifecycle scripts:

  • preinstall, install, postinstall
  • prepublish, prepare, prepublishOnly, publish, postpublish
  • preversion, version, postversion
  • pretest, test, posttest
  • prestop, stop, poststop
  • prestart, start, poststart

So the pre- and post-suffixes will be executed before install, publish, version, test, start, or stop.

How to use – test example

When you need something to run before or after the tests, you can use these hook lifecycle methods for that.

{
  "scripts": {
    "🧪 Testing": "All test suites defined",
    "pretest": "echo Starting tests, please stand by 🧬 ...",
    "test": "heft test --production",
    "posttest": "echo 🧬 TESTING is over - Hope everything worked",
  }
}

You will see that those hooks get executed before and after the tests defined for your Heft configuration.

npm test runs on the console the pre and post test commands
Here you see how the pre and post actions are executed

Of course, this works the same way with the other scripts.

Heft flags are useful but …

Do I really have to train my muscle memory to type the following each time I create a new package?

> heft test --clean --production && heft package-solution --production

We already trained it once for:

> gulp bundle --ship && gulp package-solution --ship

When we could have used:

npm run package:prod

Short, smarter, and memorable.

The Final npm script

Your final npm script section then might look like this:

{
  "scripts": {
    "📦 Packaging": "Package the SPFx project for dev and :prod for production",
    "package": "npm run clean && heft package-solution",
    "package:prod": "npm run clean && heft test --production && heft package-solution --production",

    "👩🏻‍💻 Development": "When you update your code",
    "dev": "heft start --clean",
    "dev:nobrowser": "heft start --clean --nobrowser",
    "start": "heft start --clean",

    "🧪 Testing": "All test suites defined",
    "pretest": "echo Starting tests, please stand by 🧬 ...",
    "test": "heft test --production",
    "posttest": "echo 🧬 TESTING is over - Hope everything worked",

    "🧰 Utilities": "Common Utilities",
    "clean": "heft clean",
    "webpack:eject": "heft eject-webpack"
  }
}

A clean, easy-to-read command that every developer understands.

Wrapping up

Npm scripts give you:

  • Consistency – Same commands across all projects
  • Documentation – Self-describing with emoji sections
  • Maintainability – Change once, apply everywhere
  • Pipeline simplicity – CI/CD calls npm run package:prod, not tool-specific commands

In particular, the pipeline’s simplicity is the biggest advantage. You have abstracted the project-specific details from the deployment-specific details.

Find more posts in the following categories

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.