Selective SPFx Builds in Multi-Web-Part Repos with spfx-selective-build

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.json with bundle entries for the full solution
  • one package-solution.json representing 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/**/*.ts
  • src/shared/**/*.tsx
  • src/**/*.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:

  1. read the selected profile from config/build-profiles.json
  2. filter config/config.json down to the selected bundle set
  3. derive the relevant web part source folders from manifest paths
  4. rewrite tsconfig.json so only the selected web part plus src/shared/** are compiled
  5. swap config/package-solution.json to the profile-specific package file
  6. run the requested Heft command
  7. 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:

  • build verifies the selected profile compiles for production
  • package builds 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.json
  • config/package-solution.json
  • tsconfig.json

The committed backups are:

  • config/config.backup.json
  • config/package-solution.backup.json
  • tsconfig.backup.json

The runtime behavior is straightforward:

  1. ensure backup files exist
  2. apply the selected profile
  3. run the command
  4. restore the baseline files in a finally path

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

ApproachWhat it gives youWhat becomes harder
Repo-local script onlyFast first implementation inside one solutionReuse, onboarding, consistency, shared maintenance
Reusable npm/npx CLIStandardized commands, repeatable adoption, easier updates across repositoriesYou 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-solution
npx @sudharsank/spfx-selective-build list
npx @sudharsank/spfx-selective-build build prh
npx @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:list
npm run wp:test -- prh
npm run wp:build -- prh
npm run wp:package -- gov-suite
npm run wp:create-profile -- --key my-webpart --name "My Web Part" --bundle my-web-part
npm run wp:update-profile -- --key gov-suite --bundle external-sharing-command-center-web-part
npm run wp:remove-profile -- --key gov-suite --bundle external-sharing-command-center-web-part
npm run wp:sync-backups
npm 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.json for the selected web part plus src/shared/**
  • swapping package-solution.json
  • running heft start, heft build, or heft package-solution
  • running heft test on 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.json
  • config/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 npx for 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 build for focused production compilation and keep tests separate
  • use package when you actually need the .sppkg
  • run sync-backups only 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.

Leave a Reply