sfpm
Concepts

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:

  1. Stage. Copies the package's Salesforce source and the project files it needs into dist/. The staged source sits under the sfpm.path layout. Staging clears dist/ first, and it runs before the skip check. See caching for what that means for a skipped build.
  2. Skip check. Compares the source hash with the last build. If nothing changed, the build stops here. See caching.
  3. Analyze. Reads the Apex classes and tests, field usage, and picklist usage. The results go into the artifact metadata. Data packages skip this step.
  4. build:pre hooks. See lifecycle hooks.
  5. Local checks. Runs only with --validation local or full. The compile check reports itself as skipped without Nimbus; the dependency check reports an error instead, which only fails the build at full. See validation levels.
  6. Builder. An unlocked package creates a package version in the DevHub when validation is org or full. A source package has no build step beyond staging. A data package validates export.json.
  7. Artifact metadata. Writes dist/package.json and changelog.json.
  8. build:post hooks.

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:

LevelLocal checks (Nimbus)Org validationNeeds
local (default)Compile check and dependency check. Both warn only.None.Nothing extra. Unlocked packages build as source.
orgNone.Source packages to a build org. Unlocked packages get a package version from the DevHub.A DevHub (--target-dev-hub).
fullCompile 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.path layout.
  • .forceignore, and the package's sfdx-project.json.
  • manifests/, and the optional config/, seedMetadata/, unpackagedMetadata/, and destructive/ 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 as source.
  • 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: the 04t ID. Present only when the DevHub created a package version.
  • packageId and isOrgDependent: 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:

  1. The --build-number (-b) flag.
  2. The CI run ID from the environment: GITHUB_RUN_ID, BUILD_BUILDID, CI_PIPELINE_ID, BITBUCKET_BUILD_NUMBER, CIRCLE_BUILD_NUM, or CODEBUILD_BUILD_ID.
  3. 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.

  • --tag sets the npm dist-tag. The default is latest.
  • --dry-run shows what would be published without publishing.
  • A package with no dist/package.json is skipped with a warning. Run sfpm build first.

Consuming artifacts

There are two ways to bring an artifact into a project:

  • sfpm add <package> runs pnpm add in the project. It warns when the added package has no sfpm field.
  • sfpm install and sfpm deploy artifact fetch the named artifacts into node_modules with npm install --no-save. The workspace package.json is not changed. Install then uses the artifact metadata.

For how each command uses the artifact, see deploy vs install.

Next

On this page