Caching
builderman supports task-level caching to skip expensive work when inputs and outputs haven't changed. This is useful for build-style tasks where you want to avoid re-running work when nothing has changed.
Basic Usage
Enable caching by providing cache configuration in your command:
const buildTask = task({
name: "build",
commands: {
build: {
run: "tsc",
cache: {
inputs: ["src"],
// outputs is optional; if omitted, only inputs are tracked
outputs: ["dist"],
},
},
},
})
How It Works
When caching is enabled:
- First run: The task executes normally and creates a snapshot of the input and output files
- Subsequent runs: The task compares the current state with the cached snapshot
- Cache hit: If inputs and outputs are unchanged, the task is skipped (no command execution)
- Cache miss: If anything changed, the task runs and updates the cache
Cache Inputs
Cache inputs can include:
- File paths (strings): Directories or files to track
- Artifacts: References to outputs from other tasks using
task.artifact("command") - Input resolvers: Special functions that resolve to cacheable inputs (e.g., package dependencies)
File Paths
const buildTask = task({
name: "build",
commands: {
build: {
run: "tsc",
cache: {
inputs: ["src", "package.json"],
outputs: ["dist"],
},
},
},
})
Artifacts
You can reference outputs from other tasks as cache inputs using task.artifact("command"). This creates an artifact dependency that tracks changes to the producing task's outputs.
const shared = task({
name: "shared",
cwd: "packages/shared",
commands: {
build: {
run: "npm run build",
cache: {
inputs: ["src"],
outputs: ["dist"],
},
},
},
})
const backend = task({
name: "backend",
cwd: "packages/backend",
commands: {
build: {
run: "npm run build",
cache: {
inputs: [
"src",
shared.artifact("build"), // Track changes to shared's build outputs
],
outputs: ["dist"],
},
},
},
})
When using artifacts:
- The artifact-producing task must have
cache.outputsdefined - The artifact is included in the cache key, so changes to the artifact invalidate the cache
- The consuming task automatically depends on the producing task (execution dependency)
Input Resolvers
Input resolvers are functions that resolve to cacheable inputs. They're useful for tracking package dependencies and other dynamic inputs.
For example, the @builderman/resolvers-pnpm package provides a resolver for pnpm package dependencies:
import { task } from "builderman"
import { pnpm } from "@builderman/resolvers-pnpm"
const server = task({
name: "server",
cwd: "packages/server",
commands: {
build: {
run: "pnpm build",
cache: {
inputs: [
"src",
pnpm.package(), // Automatically tracks pnpm dependencies
],
outputs: ["dist"],
},
},
},
})
The resolver automatically detects whether you're in a workspace or local package and tracks the appropriate pnpm-lock.yaml and package dependencies.
How Caching Works Internally
The cache system:
- Creates a snapshot of file metadata (modification time and size) for all files in the configured input and output paths
- For artifacts, tracks the artifact identifier from the producing task's cache
- For resolvers, includes the resolved input in the cache key
- Stores snapshots in
.builderman/cache/<version>/relative to the main process's working directory - Compares snapshots before running the task
- Writes the snapshot after successful task completion (ensuring outputs are captured)
Path Resolution
- Paths may be absolute or relative to the task's
cwd - Directories are recursively scanned for all files
- Non-existent paths are treated as empty (no files)
Cache Information in Statistics
When a task has cache configuration, its statistics include cache information:
const result = await pipeline([buildTask]).run()
const taskStats = result.stats.tasks[0]
if (taskStats.cache) {
console.log("Cache checked:", taskStats.cache.checked)
console.log("Cache hit:", taskStats.cache.hit)
console.log("Cache file:", taskStats.cache.cacheFile)
console.log("Inputs:", taskStats.cache.inputs)
console.log("Outputs:", taskStats.cache.outputs)
}
Cache Behavior
- Cache failures never break execution — if cache checking fails, the task runs normally
- Cache is written after completion — ensures outputs are captured correctly
- Cache is per task and command — each task-command combination has its own cache file
- Cache directory is versioned — stored under
v1/to allow future cache format changes
When to Use Caching
Caching is ideal for:
- Build tasks (TypeScript compilation, bundling, etc.)
- Code generation tasks
- Any expensive operation where inputs/outputs can be reliably tracked
Caching is not suitable for:
- Tasks that have side effects beyond file outputs
- Tasks that depend on external state (APIs, databases, etc.)
- Tasks where outputs are non-deterministic
Related: Learn about Skipping Tasks and Execution Statistics.