Wire your build tools
Run your own compiler or bundler before sfpm packages the output. sfpm does not compile source, and it cannot run a compiler for you mid-build.
sfpm does not compile LWC, TypeScript, or SCSS. It has no compiler for any of
them. Your project's own build tool does the compiling, such as tsc, a
bundler, or a Sass compiler. You run that tool yourself, before sfpm sees the
package.
The one rule that matters
sfpm build stages a package's source into dist/, checks whether anything
changed, and analyzes the result, all before any pre hook runs. The source
hash and the analyzed metadata used for that build are fixed at that point.
The script hook built into @b64hub/sfpm-hooks runs pre scripts in the
original package directory, not in the staged dist/ copy, so even a script
that writes compiled output there doesn't reach that build. A custom hook you
write yourself can reach the staged directory through its context, but the
hash and analysis were already computed before any hook runs, so using a hook
to add files afterward isn't a pattern this guide recommends, even where it
might appear to work for some package types.
The practical result is the same either way: a lifecycle hook isn't where you
wire in your compiler. Your compiler has to run, and leave its output on disk
under the package's source path, before you invoke sfpm build,
sfpm deploy, or sfpm install at all.
In a Turborepo workspace
If you used sfpm project init turbo to scaffold or
migrate this package, it already has an sfpm:build script, and the
generated turbo.json already has an sfpm:build task. A package you
created by hand (see the quick start's
fallback path)
doesn't get these automatically; add the same three sfpm:build /
sfpm:deploy / sfpm:install scripts yourself, matching the example below.
Add your own compile script to the package, and make the sfpm:build task
depend on it so Turbo always runs your compiler first.
In the package's package.json:
{
"name": "@myorg/my-lwc",
"version": "1.0.0",
"private": true,
"scripts": {
"compile": "tsc -p tsconfig.json",
"sfpm:build": "sfpm build my-lwc --turbo",
"sfpm:deploy": "sfpm deploy my-lwc --turbo",
"sfpm:install": "sfpm install my-lwc --turbo"
},
"sfpm": {
"packageType": "source",
"path": "force-app"
}
}Set tsconfig.json's outDir to a directory under force-app (or wherever
this package's sfpm.path points), so the compiled output lands exactly
where sfpm will stage it from.
In the workspace root turbo.json, add compile to the sfpm:build task's
dependsOn. This is the same turbo.json sfpm project init turbo generated
for you, with one task added and one dependency added:
{
"tasks": {
"compile": {
"cache": false
},
"sfpm:build": {
"cache": true,
"dependsOn": ["compile", "^sfpm:build"],
"env": ["SF_DEV_HUB", "SFPM_FORCE_BUILD", "SFDX_AUTH_URL", "SF_ACCESS_TOKEN"],
"inputs": ["$TURBO_DEFAULT$"],
"outputs": ["dist/**"]
}
}
}turbo run sfpm:build now runs compile for a package before its own
sfpm:build script runs, in addition to the existing ^sfpm:build dependency
that orders builds across packages. By the time sfpm stages the package, the
compiled files are already on disk.
If you run sfpm project init turbo again later, it regenerates the
sfpm:build task and drops the compile dependency. Add it back.
Without Turborepo
Run your compiler as a step before the sfpm command, the same way you'd run any other build step:
cd my-lwc
npm run compile
cd ..
sfpm build my-lwc --forceUse --force here. sfpm's source hash only hashes individual files, and an
LWC or Aura bundle's contents are a directory, so a change to compiled
JavaScript inside a bundle can go unnoticed and the build is skipped as
"no changes" (see Caching). The --turbo flag used
in the Turborepo path above forces the build the same way, so this only
matters when you invoke sfpm build directly.
In CI, make the compile step an explicit step before the build step, not something sfpm triggers for you.
What lifecycle hooks are still good for
packageOptions.hooks.scripts and custom hook sets (see
Lifecycle hooks) still run real scripts at
real points in the pipeline. They're the right tool for side effects around
a build or install that don't need to add new files before staging: posting
a build notification, writing a report, or calling an external system. They
are not a way to generate source that needs to end up in the package. Use
the compile-before-you-build pattern above for that.
sfpm's job is dependency-ordered orchestration and packaging of whatever is already on disk under each package's source path. Wiring in your own compiler is a normal part of your own build pipeline, a Turborepo task dependency or a preceding step, not something sfpm does or can be told to do through a hook.
Lifecycle hooks
When hooks run during build, deploy, and install, how to scope them to stages and packages, and how to write a custom hook set.
Build, validate, and publish
A CI workflow that builds packages, resolves async validation for unlocked packages, and publishes only the packages that passed. Has a known DevHub-wiring limitation at the default settings, covered below.