5.7 KiB
name, description
| name | description |
|---|---|
| pnpm-hooks | Customize resolution, config, packing, and fetching with .pnpmfile.mjs hooks, finders, and custom resolvers/fetchers |
pnpm Hooks (.pnpmfile.mjs)
pnpm hooks customize installation. Declare them in .pnpmfile.mjs (ESM, preferred) or .pnpmfile.cjs (CommonJS), located next to the lockfile (workspace root for a monorepo).
The modern format uses ESM
export const hooks = { ... }. The old CommonJSmodule.exports = { hooks }still works in.pnpmfile.cjs.
Setup
export const hooks = {
readPackage,
afterAllResolved,
updateConfig,
beforePacking,
}
Hook reference
| Hook | When | Use |
|---|---|---|
readPackage(pkg, ctx) |
after a dependency manifest is parsed | mutate a dependency's package.json (affects resolution) |
afterAllResolved(lockfile, ctx) |
after resolution | mutate the lockfile before it's written |
updateConfig(config) |
before install | mutate pnpm's settings (great with config dependencies) |
beforePacking(pkg) |
before pnpm pack/publish tarball |
customize the published manifest only |
preResolution(opts) |
after reading lockfiles, before resolution | inspect/modify lockfile objects |
importPackage(dir, opts) |
when writing to node_modules | change how packages are linked |
readPackage
Called for every package before resolution. Common uses:
function readPackage(pkg, context) {
// Add a missing peer dependency
if (pkg.name === 'some-broken-package') {
pkg.peerDependencies = { ...pkg.peerDependencies, react: '*' }
}
// Pin a transitive version
if (pkg.dependencies?.lodash) pkg.dependencies.lodash = '^4.17.21'
// Drop a problematic optional dep
delete pkg.optionalDependencies?.fsevents
// Replace a deprecated dep
if (pkg.dependencies?.['old-pkg']) {
pkg.dependencies['new-pkg'] = pkg.dependencies['old-pkg']
delete pkg.dependencies['old-pkg']
}
return pkg
}
export const hooks = { readPackage }
Mutations are not written to disk; they only affect resolution. Delete
pnpm-lock.yamlto re-resolve an already-locked dependency. Removingscriptshere does not stop a build — use theallowBuildssetting instead. To persist a change to a dependency's files, usepnpm patch.
updateConfig
Modify pnpm's own settings programmatically — most powerful when shipped in a config dependency so settings are shared across repos.
export const hooks = {
updateConfig(config) {
return Object.assign(config, {
enablePrePostScripts: false,
optimisticRepeatInstall: true,
resolutionMode: 'lowest-direct',
verifyDepsBeforeRun: 'install',
})
}
}
// Add a catalog entry from a plugin
export const hooks = {
updateConfig(config) {
config.catalogs.default ??= {}
config.catalogs.default['is-odd'] = '1.0.0'
return config
}
}
beforePacking
Customize the manifest that ends up in the published tarball without touching your local package.json.
export const hooks = {
beforePacking(pkg) {
delete pkg.devDependencies
pkg.main = './dist/index.js'
return pkg
}
}
afterAllResolved
export const hooks = {
afterAllResolved(lockfile, context) {
context.log(`Resolved ${Object.keys(lockfile.packages || {}).length} packages`)
return lockfile
}
}
Finders (pnpm list / why)
Custom predicates used via --find-by:
export const finders = {
react17: (ctx) => ctx.readManifest().peerDependencies?.react === '^17.0.0'
}
pnpm why --find-by=react17
Custom resolvers & fetchers (advanced)
Register top-level resolvers/fetchers to support new package schemes (e.g. my-protocol:pkg). Each is an object with cheap canResolve/canFetch guards plus resolve/fetch. Custom resolvers run before built-ins; custom resolution type fields must use the custom: prefix.
const resolver = {
canResolve: (dep) => dep.alias.startsWith('@company/'),
resolve: async (dep) => ({
id: `${dep.alias}@${dep.bareSpecifier}`,
resolution: { type: 'custom:cdn', cdnUrl: '...' },
}),
}
const fetcher = {
canFetch: (id, res) => res.type === 'custom:cdn',
fetch: (cafs, res, opts, fetchers) =>
fetchers.remoteTarball(cafs, { tarball: res.cdnUrl, integrity: res.integrity }, opts),
}
module.exports = { resolvers: [resolver], fetchers: [fetcher] }
hooks.fetcherswas removed in v11 — use the top-levelfetchersexport instead.
Related settings
ignorePnpmfile: false # ignore the pnpmfile entirely
pnpmfile: ['.pnpmfile.mjs'] # local pnpmfile location(s)
globalPnpmfile: ~/.pnpm/global_pnpmfile.mjs
Hooks vs Overrides
| Hooks (.pnpmfile) | Overrides (pnpm-workspace.yaml) | |
|---|---|---|
| Logic | JavaScript | declarative |
| Scope | any manifest field, config, lockfile, packing | versions |
| Use when | conditional/complex fixes | simple version pins |
Prefer overrides/packageExtensions for simple cases; use hooks for conditional logic, config sharing, or packing tweaks.
Key Points
- Prefer
.pnpmfile.mjswithexport const hooks/finders/resolvers/fetchers. - New hooks:
updateConfig(mutate settings),beforePacking(published manifest),preResolution,importPackage. - Pair
updateConfigwith config dependencies to share settings/catalogs across repos. --ignore-scriptsdoes not disable the pnpmfile; useignorePnpmfile.