SPFx 1.22 with Heft — Stylelint with autofix

SPFx 1.22 with Heft — Stylelint with autofix

Goal

Add a standalone Heft task to validate styles (CSS/SCSS) with Stylelint in an SPFx 1.22 solution, with an optional --fix parameter for autofixes. It should:

  • Run standalone via heft style-lint (with optional --fix)
  • Run via npm alias npm run style:lint and npm run style:fix
  • Optionally run automatically before a production build using an npm prebuild hook

This follows Microsoft’s guidance to use Heft’s Run Script plugin for custom steps, without modifying gulp tasks.

1) Install dependencies

npm install --save-dev stylelint stylelint-config-standard-scss

Create a basic Stylelint config at the repo root:

{
  "extends": "stylelint-config-standard-scss",
  "rules": {
    "selector-class-pattern": null,
    "custom-property-pattern": null,
    "rule-empty-line-before": null,
    "scss/at-rule-no-unknown": [
      true,
      {
        "ignoreAtRules": ["value"]
      }
    ],
    "scss/load-partial-extension": null
  }
}

2) Add the lint script

Create config/run-script/style-lint.mjs in your SPFx repo. It scans typical style locations and runs Stylelint. Pass --fix to enable autofixes.

/import { execSync } from 'child_process';

export async function runAsync({ heftTaskSession, heftConfiguration }) {
  const logger = heftTaskSession.logger;
  
  logger.terminal.writeLine('Running stylelint...');

  try {
    execSync('npx stylelint "src/**/*.scss"', {
      stdio: 'inherit',
      cwd: heftConfiguration.buildFolderPath
    });
    logger.terminal.writeLine('Stylelint passed!');
  } catch (error) {
    logger.emitError(new Error('Stylelint failed!'));
  }
}

3) Configure Heft for standalone + build

Add a dedicated style-lint phase to call it directly, and optionally hook 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": {
		"style-lint": {
			"phaseDescription": "Standalone style lint",
			"tasksByName": {
				"style-lint": {
					"taskPlugin": {
						"pluginPackage": "@rushstack/heft",
						"pluginName": "run-script-plugin",
						"options": { "scriptPath": "./config/run-script/style-lint.mjs" }
					}
				}
			}
		}
	}
}

Optional: Attach to build phase to run as part of heft build.

{
	"$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": {
				"stylelint": {
                "taskPlugin": {
                    "pluginPackage": "@rushstack/heft",
                    "pluginName": "run-script-plugin",
                    "options": { "scriptPath": "./config/run-script/stylelint.mjs" }
                    }
                }
            }
        },
        "style-lint": {
            "phaseDescription": "Standalone stylelint",
            "tasksByName": {
                "stylelint": {
                "taskPlugin": {
                    "pluginPackage": "@rushstack/heft",
                    "pluginName": "run-script-plugin",
                    "options": { "scriptPath": "./config/run-script/stylelint.mjs" }
                }
                }
            }
        }
    }        
}

4) Add npm scripts

Add convenient aliases and optional prebuild hooks.

{
	"scripts": {
		"style:lint": "heft style-lint",
		"build": "heft build",
		"prebuild": "heft style-lint"
	}
}

5) How to run

  • Standalone lint: npx heft style-lint or npm run style:lint
  • Autofix is possible
  • Dev build: npm run build
  • Production build: npm run build -- --production

npm build with style-lint

Because of the prebuild hook (if you add it), the lint task runs before the build. Alternatively, rely on the build-phase attachment so heft build includes linting.

Sample output

[style-lint] Start 
src/webparts/helloWorld/components/HelloWorld.module.scss
 12:3  Unexpected duplicate selector            no-duplicate-selectors

[style-lint] Errors found.

TL;DR

  • Add config/run-script/style-lint.mjs and wire it in config/heft.json as style-lint phase
  • Add npm style:lint (or attach to build phase)
  • Optional prebuild hook to enforce linting before builds