Files
moeka-project/.agents/skills/pnpm/references/features-hooks.md
T

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 CommonJS module.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.yaml to re-resolve an already-locked dependency. Removing scripts here does not stop a build — use the allowBuilds setting instead. To persist a change to a dependency's files, use pnpm 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.fetchers was removed in v11 — use the top-level fetchers export instead.

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.mjs with export const hooks/finders/resolvers/fetchers.
  • New hooks: updateConfig (mutate settings), beforePacking (published manifest), preResolution, importPackage.
  • Pair updateConfig with config dependencies to share settings/catalogs across repos.
  • --ignore-scripts does not disable the pnpmfile; use ignorePnpmfile.