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:
| Field | Description |
|---|---|
name | Scoped npm name, for example @myorg/core. Salesforce operations use the name without the scope. |
version | Semver version, for example 1.0.0. Build numbers are added at build time. See builds and artifacts. |
dependencies | workspace: references to other sfpm packages in the workspace. These define the dependency graph. |
managedDependencies | Map of alias to subscriber package version ID (04t) for managed packages from other publishers. |
The sfpm block holds the static settings for the package:
| Field | Description |
|---|---|
packageType | Required. unlocked, source, or data. See package types. |
path | Salesforce source directory, relative to the package directory. Defaults to . (the package root). |
packageId | Package2 container ID (0Ho prefix). sfpm sets it when it creates or finds the package in the DevHub. |
isOrgDependent | Unlocked 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. |
namespace | Namespace for the package. |
metadataDependencies | seed and unpackaged directories, relative to the package directory. Staged into the build output. |
packageOptions | Per-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",
});| Field | Description |
|---|---|
hooks | Lifecycle hook sets to register. See lifecycle hooks. |
sfdcLoginUrl | Login URL written to sfdx-project.json. Defaults to https://login.salesforce.com. |
sourceApiVersion | Source API version written to sfdx-project.json, for example 63.0. sfpm project sync --api-version overrides it. |
sourceBehaviorOptions | Source 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
- Package types: what
unlocked,source, anddatamean. - Dependency graph: how dependencies set the run order.
- Builds and artifacts: what a build produces.