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.
| Layer | Question it answers | What it skips | Where its state lives |
|---|---|---|---|
| Build skip | Did this package's source change since its last build? | Rebuilding one package. | dist/package.json in the package folder. |
| Turborepo cache | Did this task's inputs change since its last run? | Running a whole sfpm:* task. | The Turborepo cache, local or remote. |
| Install and deploy check | Does 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 orgorfullneeds apackageVersionIdin its existing output. A build made with--validation localdoes 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.
| Task | Cached | Turborepo outputs | Depends on |
|---|---|---|---|
sfpm:build | Yes | dist/** | The sfpm:build task of each dependency. |
sfpm:deploy | Yes | None | The sfpm:deploy task of each dependency. |
sfpm:install | No | None | This 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__crecord for the package. If itsChecksum__cequals the package'ssourceHash, the package is skipped with the reasonhash-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 reasonversion-installed.
Data packages have no check. They import on every run.
The default differs by command:
sfpm installchecks, and skips packages that are already installed. Pass--force(-f) to install anyway.sfpm deployandsfpm deploy artifactdefault to--force, so they deploy every package. Pass--no-forceto 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
- Deploy vs install: the install and deploy flags in detail.
- Builds and artifacts: what
dist/contains.