sfpm
Concepts

Caching

The three places sfpm and Turborepo skip work, what each one compares, and how to force a run.

sfpm skips work in three places. Each one answers a different question, and each one stores its state somewhere else.

LayerQuestion it answersWhat it skipsWhere its state lives
Build skipDid this package's source change since its last build?Rebuilding one package.dist/package.json in the package folder.
Turborepo cacheDid this task's inputs change since its last run?Running a whole sfpm:* task.The Turborepo cache, local or remote.
Install and deploy checkDoes the target org already have this version?Sending one package to the org.SfpmArtifact__c records in the target org.

1. Build skip

For metadata packages (source and unlocked), sfpm computes a SHA-256 hash of the staged source. The hash covers each component's type and name, its -meta.xml file, and, for single-file components, its source file. Bundle components (LWC and Aura) contribute only their -meta.xml. Files inside the bundle folder are not hashed, so an edit that only touches one of them does not trigger a rebuild. sfpm compares it with the sourceHash recorded in the package's dist/package.json from the last build.

If the hashes match, and the existing output meets what the current build needs, the build is skipped. The package is reported as skipped with the reason no-changes. The skip check runs after staging, so staging still happens for skipped packages.

Two details matter:

  • An unlocked package built with --validation org or full needs a packageVersionId in its existing output. A build made with --validation local does not create one, so the next org build rebuilds the package.
  • Data packages have no skip check. They build on every run.

To force a build, pass --force (-f) to sfpm build, or set SFPM_FORCE_BUILD. --turbo also forces a build. Under Turborepo, the cache decides whether to run the task at all. Use --force after editing files inside an LWC or Aura bundle, because those edits do not change the hash.

A skipped build can leave dist/ empty. Staging empties dist/ before the skip check runs, and the skip does not restore the previous output. The next sfpm publish for that package then reports No build found. The intended behavior is that a skip keeps the existing dist/ output. Until that is fixed, pass --force to sfpm build in CI jobs that publish right after building, so dist/ is populated.

2. Turborepo cache

sfpm project init turbo writes three tasks into turbo.json. Each package it migrates also gets scripts that call the CLI with --turbo. Turborepo runs those scripts and caches the results.

TaskCachedTurborepo outputsDepends on
sfpm:buildYesdist/**The sfpm:build task of each dependency.
sfpm:deployYesNoneThe sfpm:deploy task of each dependency.
sfpm:installNoNoneThis package's sfpm:build, then each dependency's sfpm:install.

sfpm:deploy is cacheable in the generated turbo.json. On a cache hit, Turborepo replays the earlier run and does not run sfpm deploy, so nothing is sent to the org. If a deploy must run on every invocation, set cache to false for sfpm:deploy in turbo.json.

Turborepo inputs come from the source directories you pick during a fresh init. Otherwise Turborepo's default input set is used.

Variables listed in a task's env are part of its cache key. For example, sfpm:build lists SF_DEV_HUB and SFPM_FORCE_BUILD. Changing one of them invalidates the cached build.

On a cache hit, Turborepo restores the outputs and does not run the command. For sfpm:build, that means dist/ comes back from the cache and sfpm does not run.

The --turbo flag forces both the build and the org check, so the build skip and the org check do not decide anything inside a Turborepo task. Turborepo decides whether the task runs.

3. Install and deploy check

Before sending a package to the target org, sfpm checks what the org already has. Two checks apply, and either one can skip the package:

  • Hash match. sfpm reads the SfpmArtifact__c record for the package. If its Checksum__c equals the package's sourceHash, the package is skipped with the reason hash-match.
  • Version match. For a package with a package version ID (04t), which covers unlocked and managed packages, sfpm checks whether that subscriber version is already installed. If it is, the package is skipped with the reason version-installed.

Data packages have no check. They import on every run.

The default differs by command:

  • sfpm install checks, and skips packages that are already installed. Pass --force (-f) to install anyway.
  • sfpm deploy and sfpm deploy artifact default to --force, so they deploy every package. Pass --no-force to run the check first.

After each successful install or deploy, sfpm writes the SfpmArtifact__c record. The @b64hub/sfpm-artifact helper package provides that object, and sfpm bootstrap installs it. When the Sfpm_Artifact_History__c object is present, sfpm also adds a history row. That object comes from the @b64hub/sfpm-ui helper package, which is part of the full bootstrap tier. If an object is missing, sfpm finds no record for that check (or writes no history row), and the package installs.

4. Across machines and CI

The build skip reads dist/ on the local machine. The org check reads the target org. Neither one moves between CI runners by itself. A fresh CI checkout has no dist/ folder, so the build skip has nothing to compare against and builds the package.

To share cached build outputs across machines, configure a Turborepo remote cache for your repository. sfpm does not host a remote cache.

Next

On this page