sfpm
Guides

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 --force

Use --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.

On this page