sfpm
Concepts

Workspace and packages

How an sfpm workspace is laid out and where each package's configuration lives.

An sfpm project is a pnpm workspace. Each Salesforce package is one workspace member with its own package.json. The sfpm block in that file is the source of truth for the package's settings. sfdx-project.json is generated from these files. Do not edit it by hand.

Layout

my-project/
├── package.json            # root workspace: scripts and devDependencies
├── pnpm-workspace.yaml     # which directories are workspace members
├── sfpm.config.ts          # settings shared by the whole workspace
├── sfdx-project.json       # generated by sfpm, do not edit
└── packages/
    ├── core/
    │   ├── package.json    # name, version, dependencies, and the sfpm block
    │   ├── force-app/      # Salesforce source (sfpm.path)
    │   └── dist/           # build output, generated
    └── app/
        ├── package.json
        └── force-app/

How packages are discovered

sfpm reads the workspace member list from pnpm-workspace.yaml.

A member counts as an sfpm package when its package.json has an sfpm object. Members without one are ignored. An sfpm block without a packageType, or a package.json without a name or version, is skipped with a warning.

The package.json

{
  "name": "@myorg/core",
  "version": "1.0.0",
  "private": true,
  "description": "Core Apex and metadata",
  "dependencies": {
    "@myorg/shared": "workspace:^1.0.0"
  },
  "managedDependencies": {
    "Nebula Logger": "04tXXXXXXXXXXXXXXX"
  },
  "sfpm": {
    "packageType": "source",
    "path": "force-app",
    "packageOptions": {
      "hooks": {
        "profiles": false
      }
    }
  }
}

These top-level fields are read by sfpm:

FieldDescription
nameScoped npm name, for example @myorg/core. Salesforce operations use the name without the scope.
versionSemver version, for example 1.0.0. Build numbers are added at build time. See builds and artifacts.
dependenciesworkspace: references to other sfpm packages in the workspace. These define the dependency graph.
managedDependenciesMap of alias to subscriber package version ID (04t) for managed packages from other publishers.

The sfpm block holds the static settings for the package:

FieldDescription
packageTypeRequired. unlocked, source, or data. See package types.
pathSalesforce source directory, relative to the package directory. Defaults to . (the package root).
packageIdPackage2 container ID (0Ho prefix). sfpm sets it when it creates or finds the package in the DevHub.
isOrgDependentUnlocked packages only. Marks the package as org-dependent. The staged sfdx-project.json entry omits the package's dependencies, and sfpm does not request asynchronous validation for it.
namespaceNamespace for the package.
metadataDependenciesseed and unpackaged directories, relative to the package directory. Staged into the build output.
packageOptionsPer-package build, install, skip, and hook settings. See lifecycle hooks.

Project settings in sfpm.config.ts

Settings that apply to the whole workspace live in sfpm.config.ts at the project root. Use defineConfig from @b64hub/sfpm-core for type checking. sfpm loads the file with jiti, so it does not need a build step.

import { defineConfig } from "@b64hub/sfpm-core";

export default defineConfig({
  sfdcLoginUrl: "https://login.salesforce.com",
  sourceApiVersion: "63.0",
});
FieldDescription
hooksLifecycle hook sets to register. See lifecycle hooks.
sfdcLoginUrlLogin URL written to sfdx-project.json. Defaults to https://login.salesforce.com.
sourceApiVersionSource API version written to sfdx-project.json, for example 63.0. sfpm project sync --api-version overrides it.
sourceBehaviorOptionsSource behavior presets written to sfdx-project.json.

Only the per-package namespace field in a package's sfpm block has an effect today. A namespace set at the top level of sfpm.config.ts is not written to sfdx-project.json.

Other packages can add their own sections to sfpm.config.ts, typed by the helper that each one exports.

sfdx-project.json is generated

sfpm project sync writes sfdx-project.json from the workspace package.json files. sfpm also rewrites the file whenever it reads the workspace, so a manual edit is overwritten the next time any sfpm command runs. The Salesforce CLI reads this file, which is why sfpm keeps it current.

sfpm project init turbo adds sfdx-project.json and dist/ to .gitignore. Keep both out of version control.

Build output

Each package's build output goes into a dist/ folder inside the package directory. dist/ holds the staged source and the artifact package.json. Build and install steps read from it. Do not edit files in dist/. Rebuild instead. See builds and artifacts.

Next

On this page