Builds and artifacts
What a build does to a package, what lands in dist/, and how artifacts are published and consumed.
A build turns a package's source into an artifact. The artifact is a plain npm
package folder in dist/. It can be published with sfpm publish, added to
another project with sfpm add, or installed by sfpm install and
sfpm deploy artifact.
What a build does
For each package, sfpm build runs these steps in order:
- Stage. Copies the package's Salesforce source and the project files it needs
into
dist/. The staged source sits under thesfpm.pathlayout. Staging clearsdist/first, and it runs before the skip check. See caching for what that means for a skipped build. - Skip check. Compares the source hash with the last build. If nothing changed, the build stops here. See caching.
- Analyze. Reads the Apex classes and tests, field usage, and picklist usage. The results go into the artifact metadata. Data packages skip this step.
build:prehooks. See lifecycle hooks.- Local checks. Runs only with
--validation localorfull. The compile check reports itself as skipped without Nimbus; the dependency check reports an error instead, which only fails the build atfull. See validation levels. - Builder. An unlocked package creates a package version in the DevHub when
validation is
orgorfull. A source package has no build step beyond staging. A data package validatesexport.json. - Artifact metadata. Writes
dist/package.jsonandchangelog.json. build:posthooks.
Org validation for source packages runs after the build steps. sfpm deploys the
package to a build org and runs its tests. Unlocked packages create their package
version during the build step. Results are collected after every build has
finished. With --async, the command returns without waiting, and a background
watcher collects the results. Check them with sfpm build status.
Validation levels
sfpm build takes --validation (short form -l), with three values:
| Level | Local checks (Nimbus) | Org validation | Needs |
|---|---|---|---|
local (default) | Compile check and dependency check. Both warn only. | None. | Nothing extra. Unlocked packages build as source. |
org | None. | Source packages to a build org. Unlocked packages get a package version from the DevHub. | A DevHub (--target-dev-hub). |
full | Compile check warns. Undeclared dependencies fail the build. | Same as org. | A DevHub. |
The default level is local. This is the default for the sfpm build command.
Local checks use the Nimbus local Apex runtime, so Nimbus must be installed on the machine that runs the build. Local checks are best effort, and an org deploy is the authoritative result. When Nimbus is not available, the compile check is reported as skipped with a warning. It does not pass silently.
The per-package checks run without a Nimbus daemon. The validation package includes
daemon support, and the CLI build configures it (daemon.enabled is true), but no
code path starts the daemon today, so sfpm build does not use one. Whether daemon
mode needs a Nimbus Pro license once it is wired up is not confirmed.
What lands in dist/
dist/ is the package's build output. Its main files are:
- The staged source, under the
sfpm.pathlayout. .forceignore, and the package'ssfdx-project.json.manifests/, and the optionalconfig/,seedMetadata/,unpackagedMetadata/, anddestructive/folders, when the package uses them.package.json, the artifact metadata.changelog.json, which holds a placeholder message. Changelog generation is disabled.
dist/package.json starts from the workspace package.json. sfpm then adds build
data under the sfpm key:
{
"name": "@myorg/core",
"version": "1.0.0",
"files": ["force-app/**", "scripts/**", "manifest/**", "config/**", "sfdx-project.json", ".forceignore", "changelog.json"],
"keywords": ["sfpm"],
"sfpm": {
"packageType": "source",
"versionNumber": "1.0.0.1712345678",
"sourceHash": "3f9c0a1e7b2d4c6f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f",
"content": {
"metadataCount": 42
}
}
}The fields under sfpm are:
packageType: the type the build produced. An unlocked package built as source is recorded assource.versionNumber: the full version with the build number, when there is one.sourceHash: the hash of the staged source. The next build compares against it.content: the analysis results, including Apex classes and tests.packageVersionId: the04tID. Present only when the DevHub created a package version.packageIdandisOrgDependent: copied from the definition of an unlocked package.
The top-level version is the base semver, with no build number. The build number
lives in sfpm.versionNumber.
Version and build number
The version in package.json is the base version, for example 1.0.0.
For source and data packages, sfpm sets the fourth version segment at build time. The value comes from the first of these that applies:
- The
--build-number(-b) flag. - The CI run ID from the environment:
GITHUB_RUN_ID,BUILD_BUILDID,CI_PIPELINE_ID,BITBUCKET_BUILD_NUMBER,CIRCLE_BUILD_NUM, orCODEBUILD_BUILD_ID. - The current Unix time, in seconds.
Unlocked packages do not take an automatic build number. The DevHub sets the
version number when it creates the package version. --build-number has no effect
on an unlocked package either.
Publishing artifacts
sfpm publish runs pnpm publish --no-git-checks from each package's dist/
folder. With no arguments, or with all, it publishes every workspace package.
--tagsets the npm dist-tag. The default islatest.--dry-runshows what would be published without publishing.- A package with no
dist/package.jsonis skipped with a warning. Runsfpm buildfirst.
Consuming artifacts
There are two ways to bring an artifact into a project:
sfpm add <package>runspnpm addin the project. It warns when the added package has nosfpmfield.sfpm installandsfpm deploy artifactfetch the named artifacts intonode_moduleswithnpm install --no-save. The workspacepackage.jsonis not changed. Install then uses the artifact metadata.
For how each command uses the artifact, see deploy vs install.
Next
- Caching: when a build is skipped.
- Deploy vs install: what each command sends to an org.
- Build command: the full flag list.