Skip to content
Petkir Blog
XLinkedinBluesky

SPFx 1.22.0 Beta 3 — Key Facts and Impressions

Code, Azure, SPFx4 min read

Manualy impressions

The 1.22.0 Beta 3 release of SPFx feels stable. Build and packaging work smoothly, and overall the developer experience is solid.

I experimented with a custom Heft task as an extension, but that didn’t behave as expected. As a result, I’m standardizing on npm scripts (for example npm run start) that wrap Heft commands instead of calling Heft directly. This keeps the workflow consistent, hides tool specifics from day‑to‑day usage, and makes it easy to add pre/post steps.

Production build strategy

I use the same approach for production builds. Rather than invoking Heft directly, I define a dedicated npm script per solution, for example:

"package-solution-prod": "node ./setGlobalConfig.js && node ./setApiPermissions.js && heft build --production && heft package-solution --production",
"set-api-permissions": "node ./setApiPermissions.js",
"set-global-config": "node ./setGlobalConfig.js"

This makes it easy to add extra steps when needed — for example, configuration updates or API permissions that are only required for publishing.

Default package.json scripts (SPFx 1.22)

These are the default scripts generated for SPFx 1.22 projects using Heft:

{
"scripts": {
"build": "heft build --clean",
"test": "heft test",
"test-only": "heft run --only test --",
"clean": "heft clean",
"deploy": "heft dev-deploy",
"start": "heft start --clean",
"build-watch": "heft build --lite",
"package-solution": "heft package-solution",
"deploy-azure-storage": "heft deploy-azure-storage",
"eject-webpack": "heft eject-webpack",
"trust-dev-cert": "heft trust-dev-cert",
"untrust-dev-cert": "heft untrust-dev-cert"
}
}

Version sync and CI/CD

Most of my builds run through GitHub Actions or Azure DevOps Pipelines, which keeps things straightforward:

  • Perform any file updates or version sync as part of the pipeline (overwrite before build, without committing).
  • Check out the repository, build the solution, and produce an artifact.

I prefer pipelines over publishing locally so I can download the generated artifact later. That way, my local environment stays clean, and only what the pipeline produced is considered an “official” build.

Historical note: Gulp → Heft

For context, the move from Gulp to Heft in the SPFx toolchain is a significant shift. Gulp has been part of SPFx since its first public release in February 2017 — nearly nine years in the ecosystem. A lot of scripts, documentation, and habits formed around Gulp. Heft modernizes the pipeline and offers better structure and extensibility, but migrations should plan for build script changes, CI updates, and developer onboarding.

Conclusion

SPFx 1.22.0 Beta 3 feels stable and reliable. With a clear CI/CD setup and flexible npm scripts wrapping Heft, the build and packaging process is straightforward — and packaging works as expected.

Appendix

setApiPermissions.js

const fs = require('fs');
const path = require('path');
const setApiPermissions = (env) => {
const packageSolutionPath = path.join(__dirname, './config/package-solution.json');
console.log(`Setting API Permissions in ${packageSolutionPath}`);
if (fs.existsSync(packageSolutionPath)) {
const data = fs.readFileSync(packageSolutionPath, 'utf8');
let packageSolution = JSON.parse(data);
if (env.ENTRA_ResourceName && env.ENTRA_ResourceAppId) {
const apiPermissions = [];
const apiPermission = {};
apiPermission.resource = env.ENTRA_ResourceName;
apiPermission.appId = env.ENTRA_ResourceAppId;
apiPermission.scope = env.ENTRA_ResourceScope || 'user_impersonation';
apiPermission.replyUrl = env.ENTRA_ResourceReplyUrl;
apiPermissions.push(apiPermission);
packageSolution.solution.webApiPermissionRequests = apiPermissions;
fs.writeFileSync(packageSolutionPath, JSON.stringify(packageSolution, null, 2), 'utf8');
console.log(`API Permissions set successfully in ${packageSolutionPath}`);
} else {
console.log(`Environment variables ENTRA_ResourceName and ENTRA_ResourceAppId are required to set API permissions.`);
}
} else {
console.log(`Package solution file not found at ${packageSolutionPath}`);
}
};
module.exports = { setApiPermissions };
// Run if called directly
if (require.main === module) {
setApiPermissions(process.env);
}

setGlobalConfig.js

const fs = require('fs');
const path = require('path');
const setGlobalConfig = (env) => {
const globalConfigPath = path.join(__dirname, './src/globalConfig.ts');
console.log(`Setting Global Configuration in ${globalConfigPath}`);
const appId = env.ENTRA_ResourceAppId;
const replyUrl = env.ENTRA_ResourceReplyUrl;
if (appId && replyUrl) {
const configContent = `export const AZURE_FUNCTION_APP_ID = "${appId}";
export const AZURE_FUNCTION_URL = "${replyUrl}";
`;
fs.writeFileSync(globalConfigPath, configContent, 'utf8');
console.log(`Global configuration set successfully in ${globalConfigPath}`);
console.log(` AZURE_FUNCTION_APP_ID: ${appId}`);
console.log(` AZURE_FUNCTION_URL: ${replyUrl}`);
} else {
console.log(`Environment variables ENTRA_ResourceAppId and ENTRA_ResourceReplyUrl are required.`);
console.log(`Skipping globalConfig.ts generation - file will use existing values.`);
}
};
module.exports = { setGlobalConfig };
// Run if called directly
if (require.main === module) {
setGlobalConfig(process.env);
}

setSPFxVersion.js

const fs = require('fs');
const path = require('path');
const setSPFxVersion = (env) => {
const packageJsonPath = path.join(__dirname, './package.json');
const packageSolutionPath = path.join(__dirname, './config/package-solution.json');
console.log(`Setting SPFx solution version in ${packageSolutionPath}`);
// Resolve build number from environment
const buildNumber = env.BUILDVersion || env.BUILD_VERSION;
if (!buildNumber) {
console.log('Environment variable BUILDVersion (or BUILD_VERSION) is required to set the 4th version segment.');
console.log('Skipping version update.');
return;
}
if (!fs.existsSync(packageJsonPath)) {
console.log(`package.json not found at ${packageJsonPath}`);
return;
}
if (!fs.existsSync(packageSolutionPath)) {
console.log(`package-solution.json not found at ${packageSolutionPath}`);
return;
}
// Read base version from package.json
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
let baseVersion = (pkg && pkg.version) ? String(pkg.version) : '1.0.0';
// Strip any pre-release/build metadata (e.g., 1.2.3-beta.1)
baseVersion = baseVersion.split('-')[0];
const parts = baseVersion.split('.');
const major = parts[0] || '1';
const minor = parts[1] || '0';
const patch = parts[2] || '0';
const newSolutionVersion = `${major}.${minor}.${patch}.${buildNumber}`;
// Update config/package-solution.json
const packageSolution = JSON.parse(fs.readFileSync(packageSolutionPath, 'utf8'));
if (!packageSolution.solution) {
packageSolution.solution = {};
}
packageSolution.solution.version = newSolutionVersion;
fs.writeFileSync(packageSolutionPath, JSON.stringify(packageSolution, null, 2), 'utf8');
console.log(`SPFx solution version set to ${newSolutionVersion} in ${packageSolutionPath}`);
};
module.exports = { setSPFxVersion };
// Run if called directly
if (require.main === module) {
setSPFxVersion(process.env);
}