npm package: @sudharsank/spfx-selective-build
Description: Reusable CLI for profile-based selective SPFx builds, packaging, backup sync, and profile scaffolding.
Latest version: 0.1.0

# `spfx-selective-build`

`spfx-selective-build` is a reusable npm/npx CLI for profile-based selective SPFx builds.

It packages a profile-based selective build pattern into an installable tool so teams do not need to keep a one-off script in each repository.

It works against the real files in your SPFx solution. The CLI reads the live `config.json`, `package-solution.json`, and `tsconfig.json`, creates backup companions when they do not exist yet, and then applies profile-specific rewrites around each command.

## What it automates

- lists available build profiles from `config/build-profiles.json`
- scopes `config/config.json` to the selected bundle set
- scopes `tsconfig.json` to the selected web part folder(s) plus `src/shared/**`
- swaps `config/package-solution.json` to the selected profile package file
- runs `heft start`, `heft test`, `heft build`, or `heft package-solution`
- restores the baseline config files after the command completes or fails
- scaffolds new profile entries and starter `package-solution.<profile>.json` files
- updates or removes bundles from existing profiles with guard rails
- synchronizes committed backup files with a single command

## Install and run

Use it directly with `npx`:

```bash
cd ./spfx-solution
npx spfx-selective-build list
npx spfx-selective-build build prh
npx spfx-selective-build package gov-suite
```

If you run from a repo root instead of the SPFx solution folder, add `--solution-dir ./spfx-solution`.

Or install it as a local dev dependency and wire npm scripts:

```bash
npm install --save-dev spfx-selective-build
```

```json
{
  "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"
  }
}
```

Then run:

```bash
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
```

`wp:start`, `wp:test`, `wp:build`, and `wp:package` are the selective commands. Your existing default `npm run build` script still behaves exactly as your project defines it unless you intentionally replace it. In a stock SPFx project, `npm run build` continues to run the full-solution Heft flow.

When you pass arguments through `npm run`, always use `--` before the command arguments. For example:

```bash
npm run wp:create-profile -- --key my-webpart --name "My Web Part" --bundle my-web-part
```

To wire those scripts automatically into the current project package, run:

```bash
npx spfx-selective-build wire
```

If you run that from a repo root that contains `./spfx-solution`, the generated scripts automatically include `--solution-dir ./spfx-solution`. If you run it inside the SPFx solution folder itself, the generated scripts use the local project directly.

If you are testing from a local filesystem path instead of an installed npm package, run wire through that local path too. The generated scripts will use that same local CLI entry:

```bash
node /absolute/path/to/spfx-selective-build/bin/spfx-selective-build.js wire \
  --wire-command "node /absolute/path/to/spfx-selective-build/bin/spfx-selective-build.js"
```

Avoid using `npx /absolute/path/to/spfx-selective-build wire` for long-lived script wiring. `npx` may execute from a temporary cache path, and that cache path is not stable for future `npm run wp:*` commands.

## First-time setup in a stock SPFx project

If your SPFx project only has the default generator output, start by creating the first profile. The CLI will:

- use the existing live SPFx files in the project
- create `config.backup.json`, `package-solution.backup.json`, and `tsconfig.backup.json` if they are missing
- create `config/build-profiles.json` if it does not exist yet
- scaffold `package-solution.<profile>.json` from the current `package-solution.json`

Example:

```bash
npx spfx-selective-build create-profile \
  --key hello-world \
  --name "Hello World" \
  --bundle hello-world-web-part
```

`--name` is the human-readable profile label. It is what shows up in `wp:list`, and it is also used as the basis for the generated starter package metadata. `--key` is the stable machine-friendly profile identifier.

## Command reference

### `list`

Lists the profiles in `config/build-profiles.json`.

### `start <profile>`

Applies a profile, runs `heft start --clean`, and restores the baseline files afterward.

### `test <profile>`

Applies a profile, runs `heft test --clean`, and restores the baseline files afterward.

Use this when you want selective test execution for the chosen profile without changing the meaning of the project's default `build` script.

### `build <profile>`

Applies a profile, runs `heft build --clean --production`, and restores the baseline files afterward.

### `package <profile>`

Applies a profile, runs `heft build --clean --production` and `heft package-solution --production`, and restores the baseline files afterward.

### `create-profile`

Creates or updates a profile entry plus a starter `package-solution.<profile>.json` file. If `build-profiles.json` does not exist yet, the command bootstraps it from the current SPFx project.

Example:

```bash
npx spfx-selective-build create-profile \
  --key gov-suite \
  --name "Governance Insights Suite" \
  --bundle global-governance-admin-web-part \
  --bundle permission-risk-heatmap-web-part
```

Optional flags:

- `--package-file <filename>`
- `--loc-key-override <bundle>=<resourceKey>`
- `--force`

### `update-profile`

Updates an existing profile by appending one or more bundles, or by changing the profile name, package file, or localized resource overrides.

Examples:

```bash
npx spfx-selective-build update-profile \
  --key gov-suite \
  --bundle external-sharing-command-center-web-part
```

```bash
npx spfx-selective-build update-profile \
  --key gov-suite \
  --name "Governance Insights Suite" \
  --package-file package-solution.governance-suite.json
```

Use this when a profile already exists and you need to add another web part to that profile without overwriting the existing bundle list.

If you recently added a new web part to the SPFx solution and `update-profile` says the bundle is unknown even though it exists in `config.json`, run:

```bash
npm run wp:sync-backups
```

That updates `config.backup.json`, which is the baseline used for validation and restore.

### `remove-profile`

Removes one or more bundles from an existing profile.

Examples:

```bash
npx spfx-selective-build remove-profile \
  --key gov-suite \
  --bundle external-sharing-command-center-web-part
```

```bash
npx spfx-selective-build remove-profile \
  --key gov-suite \
  --bundle external-sharing-command-center-web-part \
  --bundle policy-drift-dashboard-web-part
```

Validation rules:

- supports removing multiple bundles in one command
- warns if a requested bundle is not currently part of the profile
- prevents removing the last remaining bundle from a profile

### `wire`

Updates the nearest project `package.json` with the full `wp:*` script set based on the current layout.

Generated scripts:

- `wp:list`
- `wp:start`
- `wp:test`
- `wp:build`
- `wp:package`
- `wp:create-profile`
- `wp:update-profile`
- `wp:remove-profile`
- `wp:sync-backups`
- `wp:reset`

Example:

```bash
npx spfx-selective-build wire
```

For local source testing without installing the package into the project:

```bash
node /absolute/path/to/spfx-selective-build/bin/spfx-selective-build.js \
  --wire-command "node /absolute/path/to/spfx-selective-build/bin/spfx-selective-build.js" \
  wire
```

After that, you can use:

```bash
npm run wp:list
npm run wp:create-profile -- --key hello-world --name "Hello World" --bundle hello-world-web-part
npm run wp:update-profile -- --key hello-world --bundle another-web-part
npm run wp:remove-profile -- --key hello-world --bundle another-web-part
npm run wp:test -- hello-world
npm run wp:build -- hello-world
```

### `sync-backups`

Refreshes the committed baseline copies:

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

This is useful after intentional structural changes to the full solution.

### `reset`

Restores the baseline files from their backups.

## Expected project layout

The tool expects either:

- to be run inside the SPFx solution directory, or
- to be run from a repo root that contains `./spfx-solution`

Minimum expected files:

```text
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
```

Backups are created automatically from the live project files the first time they are needed. `build-profiles.json` is created automatically when you run `create-profile` in a stock SPFx project.

## Why the tool scopes `tsconfig.json`

Filtering `config.json` is not enough in a multi-web-part SPFx solution. TypeScript can still compile unrelated source trees if the root `tsconfig.json` includes them. The CLI rewrites `tsconfig.json` during the run so the selected web part compiles together with `src/shared/**`, but unrelated web parts stay out of scope.

## Practical expectation

This CLI is meant to operate on your actual SPFx solution folder, not on bundled template config files. The package uses the project’s current baseline as the source of truth and only creates backup copies to support safe restore behavior.

## Troubleshooting

### `npm run wp:*` points to a dead `npx` cache path

If your generated scripts point to a path under `.npm/_npx/...`, they were wired from a temporary `npx` cache location and will eventually break.

Do not wire long-lived project scripts with:

```bash
npx /absolute/path/to/spfx-selective-build wire
```

Use one of these instead:

```bash
npx spfx-selective-build wire
```

or, for local source testing:

```bash
node /absolute/path/to/spfx-selective-build/bin/spfx-selective-build.js \
  --wire-command "node /absolute/path/to/spfx-selective-build/bin/spfx-selective-build.js" \
  wire
```

If the project is already wired to a dead cache path, rerun `wire` with one of the commands above to rewrite the `wp:*` scripts.

### Host path works locally but not inside Docker or a dev container

If your script points to a path like `/Users/<name>/...`, that path exists on the host machine, not inside the container.

In a containerized SPFx project, install the package inside the container filesystem or use a tarball mounted into the container, then re-run:

```bash
npx spfx-selective-build wire
```

After rewiring, `package.json` should reference either `spfx-selective-build` or a path that exists inside the container.

### `npm install ./spfx-selective-build-0.1.0.tgz` hangs or tries to hit the registry

If `npm install` hangs and logs show requests like:

```text
UNABLE_TO_GET_ISSUER_CERT_LOCALLY
```

then the environment has a TLS trust problem, often caused by a corporate proxy or custom certificate authority.

If you can fix trust for Node, configure the container or shell with your CA bundle, for example with `NODE_EXTRA_CA_CERTS`, and retry the install.

If you only need to test the package and want to avoid registry access, unpack the tarball manually:

```bash
mkdir -p node_modules/spfx-selective-build node_modules/.bin
tar -xzf spfx-selective-build-0.1.0.tgz \
  -C node_modules/spfx-selective-build \
  --strip-components=1 package
ln -sf ../spfx-selective-build/bin/spfx-selective-build.js node_modules/.bin/spfx-selective-build
chmod +x node_modules/spfx-selective-build/bin/spfx-selective-build.js
```

Then wire and test normally:

```bash
npx spfx-selective-build wire
npm run wp:list
```

### `wp:build` fails with Sass embedded compiler errors on Linux ARM

If Heft fails with an error like this:

```text
Embedded Dart Sass couldn't find the embedded compiler executable.
Please make sure the optional dependency sass-embedded-linux-arm64 is installed in node_modules.
```

then the selective build command is working, but the underlying SPFx toolchain is missing an optional platform-specific Sass package.

This is a project dependency issue, not a profile-selection issue. Fix the SPFx project dependencies first, then rerun the profile command.

Typical options:

- run a normal `npm install` after TLS and registry access are working
- explicitly install the platform-specific optional dependency required by the environment
- verify the container or build agent architecture matches the installed optional package

The CLI now restores `config.json`, `package-solution.json`, and `tsconfig.json` even when Heft fails, so a failing Sass plugin should not leave the solution in a filtered state.

### `npm run wp:list` works but `wp:build` fails because `build-profiles.json` is missing

That is expected on a stock SPFx project before the first profile exists.

Create the first profile from the live project files:

```bash
npm run wp:create-profile -- --key hello-world --name "Hello World" --bundle hello-world-web-part
```

That command also creates:

- `config/build-profiles.json`
- `config/config.backup.json`
- `config/package-solution.backup.json`
- `tsconfig.backup.json`
- `config/package-solution.<profile>.json`