SPFx 1.22 with Heft — resize images
Goal
Add a standalone Heft task to resize images in an SPFx 1.22 solution. It should:
- Run standalone via
heft image-resize - Run via npm alias
npm run image:resize - Run automatically before a production build (
npm run build -- --production) using an npmprebuildhook
This follows Microsoft’s guidance to use Heft’s Run Script plugin for custom steps, without modifying gulp tasks.
1) Install dependencies
npm install -D sharp
2) Add the resize script
Create config/run-script/image-resize.mjs in your SPFx repo. It crawls image folders, resizes large images, and optimizes them in place.
// Minimal image resize task for Heft Run Script plugin
import { readdir, mkdir, stat } from "node:fs/promises";
import { existsSync, createReadStream, createWriteStream } from "node:fs";
import { join, extname, dirname } from "node:path";
import sharp from "sharp";
const CONFIG = {
// Adjust to your SPFx repository layout
sourceDirs: [
"./assets/images", // top-level assets
"./src/webparts" // scan web part folders recursively
],
includeExts: [".jpg", ".jpeg", ".png", ".webp"],
maxWidth: 1600,
jpegQuality: 82,
webpQuality: 82,
convertToWebp: false, // set to true to convert non-webp → webp
minBytesToProcess: 200 * 1024, // skip small files
dryRun: false
};
export async function runAsync() {
console.log("[image-resize] Start");
const files = [];
for (const dir of CONFIG.sourceDirs) {
await gatherImages(dir, files);
}
if (files.length === 0) {
console.log("[image-resize] No candidate images found.");
return;
}
console.log(`[image-resize] Found ${files.length} image(s)`);
let ok = 0;
for (const file of files) {
try {
await processImage(file);
ok++;
} catch (e) {
console.warn(`[image-resize] Skip ${file}: ${e?.message || e}`);
}
}
console.log(`[image-resize] Done. Processed: ${ok}/${files.length}`);
}
async function gatherImages(dir, out) {
try {
const entries = await readdir(dir, { withFileTypes: true });
for (const e of entries) {
const p = join(dir, e.name);
if (e.isDirectory()) await gatherImages(p, out);
else if (CONFIG.includeExts.includes(extname(e.name).toLowerCase())) out.push(p);
}
} catch {
// ignore missing dirs
}
}
async function processImage(filePath) {
const st = await stat(filePath);
if (st.size < CONFIG.minBytesToProcess) return; // already small
const img = sharp(filePath);
const meta = await img.metadata();
let pipeline = img.rotate(); // EXIF-aware
if ((meta.width || 0) > CONFIG.maxWidth) {
pipeline = pipeline.resize({ width: CONFIG.maxWidth, withoutEnlargement: true, fit: "inside" });
}
if (CONFIG.dryRun) {
console.log(`[image-resize] DRY ${filePath}: ${meta.width}x${meta.height}`);
return;
}
const ext = extname(filePath).toLowerCase();
if (CONFIG.convertToWebp && ext !== ".webp") {
const outPath = filePath.replace(/\.(jpg|jpeg|png)$/i, ".webp");
await ensureDir(dirname(outPath));
await pipeline.webp({ quality: CONFIG.webpQuality }).toFile(outPath);
console.log(`[image-resize] OK ${outPath}`);
return;
}
const tmp = `${filePath}.tmp`;
if (ext === ".jpg" || ext === ".jpeg") {
await pipeline.jpeg({ quality: CONFIG.jpegQuality, mozjpeg: true }).toFile(tmp);
} else if (ext === ".png") {
await pipeline.png({ compressionLevel: 9, adaptiveFiltering: true }).toFile(tmp);
} else if (ext === ".webp") {
await pipeline.webp({ quality: CONFIG.webpQuality }).toFile(tmp);
} else {
return; // unknown
}
await replaceFile(tmp, filePath);
console.log(`[image-resize] OK ${filePath}`);
}
async function ensureDir(dir) {
if (!existsSync(dir)) await mkdir(dir, { recursive: true });
}
async function replaceFile(from, to) {
await new Promise((res, rej) => {
const rs = createReadStream(from);
const ws = createWriteStream(to);
rs.on("error", rej);
ws.on("error", rej);
ws.on("close", res);
rs.pipe(ws);
});
}
3) Configure Heft for standalone + build
Extend the SPFx rig and add a dedicated image-resize phase. This lets you call heft image-resize directly, and you can also hook it into npm run build using npm’s prebuild hook.
{
"$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json",
"extends": "@microsoft/spfx-web-build-rig/profiles/default/config/heft.json",
"phasesByName": {
"image-resize": {
"phaseDescription": "Standalone image resize",
"tasksByName": {
"image-resize": {
"taskPlugin": {
"pluginPackage": "@rushstack/heft",
"pluginName": "run-script-plugin",
"options": { "scriptPath": "./config/run-script/image-resize.mjs" }
}
}
}
}
}
}
Optional: You can also attach the same task inside the SPFx build phase if you want it to run as part of heft build. Many teams simply rely on the npm prebuild hook below to guarantee it runs before any npm run build.
4) Add npm scripts
In your SPFx package.json, add convenient aliases and a prebuild hook so image resizing always runs before npm run build.
{
"scripts": {
"image:resize": "heft image-resize",
"build": "heft build",
"build:prod": "heft build --production",
"prebuild": "heft image-resize"
}
}
5) How to run
- Standalone:
npx heft image-resizeornpm run image:resize

- Dev build:
npm run build
- Production build:
npm run build:prodornpm run build -- --production
Because of the prebuild hook, the resize task will always run before the build.
Sample output
[image-resize] Start
[image-resize] Found 12 image(s)
[image-resize] OK ./src/webparts/mywp/assets/banner.png
[image-resize] OK ./assets/images/hero.jpg
[image-resize] Done. Processed: 2/12
6) Add the image resize as a build task:
{
"$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"
}
}
},
"image-resize": {
"taskPlugin": {
"pluginPackage": "@rushstack/heft",
"pluginName": "run-script-plugin",
"options": { "scriptPath": "./config/run-script/image-resize.mjs" }
}
}
}
},
"image-resize": {
"phaseDescription": "Standalone image resize",
"tasksByName": {
"image-resize": {
"taskPlugin": {
"pluginPackage": "@rushstack/heft",
"pluginName": "run-script-plugin",
"options": { "scriptPath": "./config/run-script/image-resize.mjs" }
}
}
}
}
}
}
TL;DR
- Add
config/run-script/image-resize.mjsand wire it inconfig/heft.jsonasimage-resizephase - Add npm
image:resizeand aprebuildhook or add in build task - Run
npm run build -- --production— the resize task runs first
