Large SPFx solutions tend to get harder to build as they grow. In a multi-web-part repository, one unfinished web part can block another team’s packaging work. A focused production build can get dragged into unrelated TypeScript errors. And if developers start hand-editing config.json, tsconfig.json, or package-solution.json just to work on one target, the repo becomes fragile very quickly.
That is the problem this selective build pattern is meant to solve.
This article explains the engineering problem inside multi-web-part SPFx repositories. It discusses the repo-local script that proved the pattern. It also covers why packaging that workflow as spfx-selective-build makes it easier to reuse across projects. The standalone package repository now lives at github.com/sudharsank/spfx-selective-build. The important part is not that a package exists. The important part is that the package captures a build and packaging pattern that already worked inside a real SPFx codebase.
The Bottleneck in Multi-Web-Part SPFx Repositories
Many SPFx solutions eventually converge on a structure like this:
- multiple web parts under
src/webparts/ - shared services, helpers, contracts, and UI under
src/shared/ - one
config/config.jsonwith bundle entries for the full solution - one
package-solution.jsonrepresenting one package shape at a time - multiple packaging targets for SKUs, environments, or deployment scenarios
That structure is reasonable, but it creates friction once several teams or feature areas move in parallel.
Typical failure modes look like this:
- one web part is incomplete, but another team still needs a clean production build
- unrelated TypeScript errors in another web part block the task at hand
- packaging one solution profile requires manual file swapping
- developers spend time debugging code they are not touching
- temporary config edits get left behind and confuse the next person
In other words, the build process becomes solution-wide even when the work is not.
If a team is not ready to split a large SPFx solution into separate packages or repositories, it still needs a practical middle ground: build or package one web part at a time, keep shared code available, and leave the repository in a known-good state afterward.
The First Working Version Was Repo-Local
Before there was a reusable package, there was a repository-specific script: spfx-webpart-cli.js.
That script was the first proof that the pattern could be made reliable inside an SPFx solution. It temporarily rewrote the inputs SPFx and Heft use, ran the requested command, and restored the original files at the end.
At a high level, the script proved four ideas:
- profile-driven selection from
build-profiles.json - selective bundle filtering in
config/config.json - selective TypeScript scoping in
tsconfig.json - guaranteed restore behavior after the command finishes
That sequence matters. spfx-selective-build is useful because it packages a workflow that was already proven under real repository pressure, not because it invents a new theory of SPFx builds.
Why Filtering config.json Alone Is Not Enough
This is the most important technical point in the whole pattern.
In an SPFx solution, filtering config/config.json controls which bundles are generated. It does not fully control which source files TypeScript will compile. If the root tsconfig.json still includes the entire source tree, unrelated web part folders can stay in scope even after bundle filtering.
That means a build can still fail because of code that should have been out of scope.
So if the goal is true selective build isolation, filtering config.json alone is not enough. You also need to scope tsconfig.json.
What tsconfig.json scoping needs to include
The working pattern is to rewrite tsconfig.json so it includes:
src/shared/**/*.tssrc/shared/**/*.tsxsrc/**/*.d.ts- only the selected web part folder or folders
For example, if the active profile targets permissionRiskHeatmap, the effective include list becomes:
{ "include": [ "src/shared/**/*.ts", "src/shared/**/*.tsx", "src/**/*.d.ts", "src/webparts/permissionRiskHeatmap/**/*.ts", "src/webparts/permissionRiskHeatmap/**/*.tsx" ]}
This is what makes selective SPFx builds credible in a shared repository.
It keeps shared code in scope, which the selected web part genuinely depends on, while keeping unrelated web parts out of the TypeScript compilation boundary. Without that scoping, a multi-web-part SPFx repo still behaves like an all-or-nothing build.
How the Selective Build Flow Works
Once both config filtering and TypeScript scoping are in place, the execution flow is straightforward. The repo-local script and the reusable spfx-selective-build package follow the same steps:
- read the selected profile from
config/build-profiles.json - filter
config/config.jsondown to the selected bundle set - derive the relevant web part source folders from manifest paths
- rewrite
tsconfig.jsonso only the selected web part plussrc/shared/**are compiled - swap
config/package-solution.jsonto the profile-specific package file - run the requested Heft command
- restore the baseline files
That flow is simple enough to reason about, but it solves several kinds of friction at once:
- selective SPFx build isolation
- selective packaging for one profile or suite
- less manual config editing in shared repos
- safer day-to-day workflows for parallel teams
Why wp:build Should Run heft build, Not heft test
Another useful lesson from this pattern is that build-only flows should stay build-only.
For the wp:build scenario, the correct command is:
heft build --clean --production
Not:
heft test --clean --production
That distinction matters because heft test can pull unrelated Jest suites into the run. In a multi-web-part SPFx repository, that defeats the whole point of selective isolation. You are no longer checking whether the selected web part and its shared dependencies compile correctly. You are reintroducing solution-wide test noise into a focused build.
Using heft build for wp:build keeps the contract clear:
buildverifies the selected profile compiles for productionpackagebuilds and emits the.sppkg- test execution remains an explicit, separate concern
That separation becomes even more important when different web parts are at different levels of maturity.
The Backup-and-Restore Pattern That Keeps the Repo Safe
Selective builds only stay safe if the repository always returns to a known baseline.
The script and the package both handle this with a backup-and-restore pattern around the mutable files:
config/config.jsonconfig/package-solution.jsontsconfig.json
The committed backups are:
config/config.backup.jsonconfig/package-solution.backup.jsontsconfig.backup.json
The runtime behavior is straightforward:
- ensure backup files exist
- apply the selected profile
- run the command
- restore the baseline files in a
finallypath
That last step is what makes the workflow team-safe.
If restoration does not happen reliably, the repository can be left in a filtered state after:
- a successful build
- a failed build
- an interrupted run
- a local experiment that never got cleaned up
When that happens, the next developer inherits a working directory that no longer reflects the real baseline. The backup-and-restore pattern prevents that drift and turns selective rewriting into a repeatable workflow instead of a risky shortcut.
Why Package the Pattern as spfx-selective-build
The repo-local script solved the problem inside one repository. But repo-local tooling has an obvious limit: every new SPFx project has to copy it, adapt it, and maintain it.
That creates a familiar kind of tooling drift:
- one team fixes a bug that another team never picks up
- command names diverge
- assumptions about folder layout get hard-coded differently
- improvements become harder to share cleanly
Packaging the pattern as spfx-selective-build changes that.
The package is now maintained in its own standalone repository:
Instead of copying a one-off script, teams can install or run a reusable npm package that already knows how to:
- apply build profiles
- scope
config.json - scope
tsconfig.json - swap package metadata
- restore backups safely
- scaffold new profiles
- sync baseline backups intentionally
The package also supports --solution-dir, which makes it easier to reuse from different repository layouts instead of assuming one fixed script location.
One practical boundary is worth calling out clearly: the selective behavior lives behind the wp:* commands. A default npm run build script still does whatever the project already defines. In most SPFx solutions, that means the normal full-solution Heft build and test flow stays unchanged unless the team intentionally replaces it. The selective path is wp:start, wp:test, wp:build, and wp:package, not the stock build script.
Repo-Local Script vs Reusable npm/npx Automation
| Approach | What it gives you | What becomes harder |
|---|---|---|
| Repo-local script only | Fast first implementation inside one solution | Reuse, onboarding, consistency, shared maintenance |
| Reusable npm/npx CLI | Standardized commands, repeatable adoption, easier updates across repositories | You still need a repository that follows the expected SPFx structure |
The key point is that spfx-selective-build does not replace the original pattern. It standardizes and distributes it.
Practical spfx-selective-build Commands
The package stays intentionally small in scope. Its command set maps directly to real SPFx tasks.
Run it directly with npx
cd ./spfx-solutionnpx @sudharsank/spfx-selective-build listnpx @sudharsank/spfx-selective-build build prhnpx @sudharsank/spfx-selective-build package gov-suite
For local development:
npx @sudharsank/spfx-selective-build start prh
To run tests only for the selected profile:
npx @sudharsank/spfx-selective-build test prh
To scaffold a new profile:
npx @sudharsank/spfx-selective-build create-profile \ --key gov-suite \ --name "Governance Insights Suite" \ --bundle global-governance-admin-web-part \ --bundle permission-risk-heatmap-web-part
Here, --key is the stable profile identifier and --name is the human-readable label shown in profile listings and starter package metadata.
To add another web part to an existing profile later:
npx @sudharsank/spfx-selective-build update-profile \ --key gov-suite \ --bundle external-sharing-command-center-web-part
If a newly added web part exists in config.json but not yet in config.backup.json, refresh the baseline first:
npx @sudharsank/spfx-selective-build sync-backups
To remove one or more web parts from an existing profile:
npx @sudharsank/spfx-selective-build remove-profile \ --key gov-suite \ --bundle external-sharing-command-center-web-part
The removal flow protects against leaving a profile empty. At least one web part must remain in every profile.
To refresh committed backups after intentional full-solution changes:
npx @sudharsank/spfx-selective-build sync-backups
To restore working files back to baseline:
npx @sudharsank/spfx-selective-build reset
Install it as a dev dependency and wire npm scripts
Install the package:
npm install --save-dev @sudharsank/spfx-selective-build
Then add scripts like these:
{ "scripts": { "wp:list": "spfx-selective-build list --solution-dir ./spfx-solution", "wp:start": "spfx-selective-build start --solution-dir ./spfx-solution", "wp:test": "spfx-selective-build test --solution-dir ./spfx-solution", "wp:build": "spfx-selective-build build --solution-dir ./spfx-solution", "wp:package": "spfx-selective-build package --solution-dir ./spfx-solution", "wp:create-profile": "spfx-selective-build create-profile --solution-dir ./spfx-solution", "wp:update-profile": "spfx-selective-build update-profile --solution-dir ./spfx-solution", "wp:remove-profile": "spfx-selective-build remove-profile --solution-dir ./spfx-solution", "wp:sync-backups": "spfx-selective-build sync-backups --solution-dir ./spfx-solution", "wp:reset": "spfx-selective-build reset --solution-dir ./spfx-solution" }}
And run them like this:
npm run wp:listnpm run wp:test -- prhnpm run wp:build -- prhnpm run wp:package -- gov-suitenpm run wp:create-profile -- --key my-webpart --name "My Web Part" --bundle my-web-partnpm run wp:update-profile -- --key gov-suite --bundle external-sharing-command-center-web-partnpm run wp:remove-profile -- --key gov-suite --bundle external-sharing-command-center-web-partnpm run wp:sync-backupsnpm run wp:reset
Notice the extra -- when passing arguments through npm run. That separator is required so flags like --key, --name, and --bundle reach the selective-build command instead of being consumed by npm itself.
This is the practical reuse layer spfx-selective-build adds. Teams no longer need to keep a custom selective build script alive in every SPFx repository.
What spfx-selective-build Automates
spfx-selective-build automates the repetitive, mechanical parts of the workflow:
- reading profiles from
build-profiles.json - validating requested bundle keys
- filtering
config/config.json - deriving web part folders from manifest paths
- rewriting
tsconfig.jsonfor the selected web part plussrc/shared/** - swapping
package-solution.json - running
heft start,heft build, orheft package-solution - running
heft teston a selected profile when requested - restoring baseline files after completion or failure
- scaffolding profile entries and starter package files
- updating existing profiles with additional bundle members
- removing bundle members while preserving at least one bundle per profile
- syncing committed backup files intentionally
That is exactly the part automation should own: the repetitive repository manipulation that is easy to get wrong by hand.
What Still Requires Human Review
Even with reliable npm/npx automation, some decisions still belong to the team:
- final package metadata quality
- solution naming and package naming
- solution IDs and feature IDs governance
- descriptions, URLs, and publisher metadata
- versioning and release decisions
- whether a target should be a single-web-part profile or a suite profile
The create-profile workflow can scaffold a starting point, including generated GUIDs and a starter package-solution.<profile>.json, and update-profile can append new web parts to an existing profile later. But the tool still does not make release-quality decisions for you. That is the right boundary.
How to Adopt This in Another SPFx Repository
If another SPFx repository has the same underlying problem, the adoption path is fairly direct.
1. Keep the expected structure
At minimum, the solution should have:
spfx-solution/ tsconfig.json tsconfig.backup.json config/ build-profiles.json config.json config.backup.json package-solution.json package-solution.backup.json package-solution.<profile>.json
2. Commit real baseline backups
Create and commit these files:
config/config.backup.jsonconfig/package-solution.backup.json- tsconfig.backup.json
Those are not disposable artifacts. They are the repository baseline that spfx-selective-build restores after each selective run.
3. Define profiles explicitly
Create config/build-profiles.json with entries like this:
{ "profiles": { "my-webpart": { "name": "My Web Part", "bundles": [ "my-web-part" ], "packageSolution": "package-solution.my-web-part.json" } }, "defaultProfile": "my-webpart"}
Each profile tells the CLI:
- which bundle entries to keep
- which package definition to swap in
- which logical target the profile represents
4. Keep shared code in a predictable place
This pattern works best when:
- reusable code lives under
src/shared/** - web parts stay isolated under
src/webparts/<folder> - web parts do not import directly from each other
That layout is what allows spfx-selective-build to scope TypeScript safely without trying to infer arbitrary repository relationships.
5. Choose your adoption model
You have two practical options:
- use
npxfor quick adoption and validation - install as a dev dependency and wire stable npm scripts for the team
For most teams, the second option is better once the workflow is proven locally because it makes the commands discoverable and repeatable.
Honest Boundaries
This pattern is useful, but it is not a silver bullet.
It does help with:
- selective SPFx builds in shared repos
- focused packaging for one profile or suite
- protecting developers from unrelated web part breakage
- standardizing automation across SPFx repositories
It does not:
- replace a true multi-package architecture
- infer every possible transitive dependency outside the intended structure
- fix poor repository boundaries automatically
- decide release metadata for you
- turn all tests into isolated per-web-part ownership models
If one web part imports code directly from another web part folder, this approach will expose that as a structural problem. That is a feature, not a bug. The selective build pattern works best when the repository already has a clean separation between shared code and web-part-specific code.
Practical Advice for Teams
If you adopt this pattern, a few habits make it much more reliable:
- keep
src/shared/**as the only place for cross-web-part reusable code - avoid direct imports from one web part folder into another
- treat backup files as committed baseline assets, not temporary files
- keep profiles short, explicit, and stable
- use
buildfor focused production compilation and keep tests separate - use
packagewhen you actually need the.sppkg - run
sync-backupsonly when the full-solution baseline intentionally changes - review generated package metadata before release
Conclusion
Selective SPFx builds are not about convenience alone. They solve a real engineering problem in multi-web-part repositories: how to keep work focused when the solution is shared, the codebase is active, and not every web part is ready at the same time. The original repo-local script proved that the pattern works: filter the active bundles, scope TypeScript to the selected web part plus shared code, swap profile-specific package metadata, run the right Heft command, and restore the baseline afterward.
spfx-selective-build turns that proven pattern into reusable npm/npx automation. That makes it easier for other SPFx repositories to adopt the workflow without copying and maintaining a one-off script in each codebase.
For teams that need better build isolation now but are not ready for a full repository redesign, that is a practical middle ground. It includes focused builds and safer packaging. There is less workflow friction without pretending the underlying architecture problem has disappeared.
Happy Sharing…