* feat: report lockfile verification progress The lockfile resolution verifier introduced in #11705 runs an unbounded registry round-trip on cache miss and was previously silent — on a cold registry cache users saw nothing for several seconds. Emit pnpm:lockfile-verification log events (started/done) around the actual verification pass and render them in the default reporter as a transient progress line that collapses into a final "verified" summary with entry count and elapsed time. The cached short-circuit stays silent. * feat: include lockfile path in verification log and render when non-standard Add `lockfilePath` to the `pnpm:lockfile-verification` event payload so consumers always know which lockfile a `started`/`done` pair refers to. In the default reporter, render the path in the message only when the lockfile lives outside the workspace root (or, for non-workspace installs, outside cwd) — the common case stays uncluttered, while custom `lockfileDir` setups now surface in the verification line. * feat: name what the lockfile verification actually checks in the rendered message "Verifying lockfile" was opaque about *what* was being verified. Reword the rendered messages to explicitly name the check ("supply-chain policies"), so users on a cold-cache pause understand what's happening instead of just seeing the pause. * fix: skip lockfile verification emission for empty candidate set A non-empty lockfile.packages whose snapshots all fail name/version extraction would still emit a "Verifying lockfile (0 entries)" line even though no verifier work runs. Bail before emission when the candidate map is empty so the no-op branch stays silent, matching the contract for the other no-op branches (empty verifiers, no lockfile.packages). * fix(reporter): always close out the verifying-lockfile frame Address two Copilot review points on #11712: 1. The verifier emitted `started` but no terminal event when violations were found or when the registry fan-out threw, leaving "Verifying lockfile…" as the last frame for that block in ansi-diff mode (and an unmatched line in CI logs). Add a `failed` status to the logger, wrap the fan-out in try/finally so a terminal event is emitted on every exit path that emitted `started`, and render a brief failure line so the spinner-style frame is replaced before the PnpmError block prints. 2. The path-suppression heuristic used strict `===` between path.dirname(lockfilePath) and expectedDir, which broke on trailing separators and slash-direction differences. Switch to a path.relative-based check so a workspaceDir like `/repo/` or a Windows path with mixed slashes still correctly suppresses the redundant "at <path>" suffix. * docs: update lockfile verification logging behavior The lockfile verifier now emits log events during the registry round-trip pass, improving user visibility into the process.
@pnpm/installing.deps-installer
Fast, disk space efficient installation engine. Used by pnpm
Install
Install it via npm.
pnpm add @pnpm/installing.deps-installer
It also depends on @pnpm/logger version 1, so install it as well via:
pnpm add @pnpm/logger@1
API
mutateModules(importers, options)
TODO
link(linkFromPkgs, linkToModules, [options])
Create symbolic links from the linked packages to the target package's node_modules (and its node_modules/.bin).
Arguments:
linkFromPkgs- String[] - paths to the packages that should be linked.linkToModules- String - path to the dependent package'snode_modulesdirectory.options.reporter- Function - A function that listens for logs.
linkToGlobal(linkFrom, options)
Create a symbolic link from the specified package to the global node_modules.
Arguments:
linkFrom- String - path to the package that should be linked.globalDir- String - path to the global directory.options.reporter- Function - A function that listens for logs.
linkFromGlobal(pkgNames, linkTo, options)
Create symbolic links from the global pkgNames to the linkTo/node_modules folder.
Arguments:
pkgNames- String[] - packages to link.linkTo- String - package to link to.globalDir- String - path to the global directory.options.reporter- Function - A function that listens for logs.
storeStatus([options])
Return the list of modified dependencies.
Arguments:
options.reporter- Function - A function that listens for logs.
Returns: Promise<string[]> - the paths to the modified packages of the current project. The paths contain the location of packages in the store,
not in the projects node_modules folder.
storePrune([options])
Remove unreferenced packages from the store.
Hooks
Hooks are functions that can step into the installation process. All hooks can be provided as arrays to register multiple hook functions.
readPackage(pkg: Manifest, context): Manifest | Promise<Manifest>
This hook is called with every dependency's manifest information.
The modified manifest returned by this hook is then used by @pnpm/installing.deps-installer during installation.
An async function is supported.
Arguments:
pkg- The dependency's package manifest.context.log(message)- A function to log debug messages.
Example:
const { installPkgs } = require('@pnpm/installing.deps-installer')
installPkgs({
hooks: {
readPackage: [readPackageHook]
}
})
function readPackageHook (pkg, context) {
if (pkg.name === 'foo') {
context.log('Modifying foo dependencies')
pkg.dependencies = {
bar: '^2.0.0',
}
}
return pkg
}
preResolution(context, logger): Promise<void>
This hook is called after reading lockfiles but before resolving dependencies. It can modify lockfile objects.
Arguments:
context.wantedLockfile- The lockfile frompnpm-lock.yaml.context.currentLockfile- The lockfile fromnode_modules/.pnpm/lock.yaml.context.existsCurrentLockfile- Boolean indicating if current lockfile exists.context.existsNonEmptyWantedLockfile- Boolean indicating if wanted lockfile exists and is not empty.context.lockfileDir- Directory containing the lockfile.context.storeDir- Location of the store directory.context.registries- Map of registry URLs.logger.info(message)- Log an informational message.logger.warn(message)- Log a warning message.
afterAllResolved(lockfile: Lockfile): Lockfile | Promise<Lockfile>
This hook is called after all dependencies are resolved. It receives and returns the resolved lockfile object. An async function is supported.
Arguments:
lockfile- The resolved lockfile object that will be written topnpm-lock.yaml.
Custom Resolvers and Fetchers
Custom resolvers and fetchers allow you to implement custom package resolution and fetching logic for new package identifier schemes (like my-protocol:package-name). These are defined as top-level exports in your .pnpmfile.cjs:
- Custom Resolvers: Convert package descriptors (e.g.,
foo@^1.0.0) into resolutions - Custom Fetchers: Completely handle fetching for custom package types
Custom Resolver Interface:
interface CustomResolver {
// Resolution phase
canResolve?: (wantedDependency: WantedDependency) => boolean | Promise<boolean>
resolve?: (wantedDependency: WantedDependency, opts: ResolveOptions) => ResolveResult | Promise<ResolveResult>
// Force resolution check
shouldRefreshResolution?: (depPath: string, pkgSnapshot: PackageSnapshot) => boolean | Promise<boolean>
}
Custom Fetcher Interface:
interface CustomFetcher {
// Fetch phase - complete fetcher replacement
canFetch?: (pkgId: string, resolution: Resolution) => boolean | Promise<boolean>
fetch?: (cafs: Cafs, resolution: Resolution, opts: FetchOptions, fetchers: Fetchers) => FetchResult | Promise<FetchResult>
}
Custom Resolver Methods:
canResolve(wantedDependency)- Returnstrueif this resolver can resolve the given package descriptorresolve(wantedDependency, opts)- Resolves a package descriptor to a resolution. Should return an object withidandresolutionshouldRefreshResolution(depPath, pkgSnapshot)- Returntrueto trigger full resolution of all packages (skipping the "Lockfile is up to date" optimization). ThedepPathis the package identifier (e.g.,lodash@4.17.21) andpkgSnapshotprovides direct access to the lockfile entry (resolution, dependencies, etc.).
Custom Fetcher Methods:
canFetch(pkgId, resolution)- Returnstrueif this fetcher can handle fetching for the given resolutionfetch(cafs, resolution, opts, fetchers)- Completely handles fetching the package contents. Receives the content-addressable file system (cafs), the resolution, fetch options, and pnpm's standard fetchers for delegation. Must return a FetchResult with the package files.
Example - Reusing pnpm's fetcher utilities:
// .pnpmfile.cjs
const customResolver = {
canResolve: (wantedDependency) => {
return wantedDependency.alias.startsWith('company-cdn:')
},
resolve: async (wantedDependency, opts) => {
const actualName = wantedDependency.alias.replace('company-cdn:', '')
const version = await fetchVersionFromCompanyCDN(actualName, wantedDependency.bareSpecifier)
return {
id: `company-cdn:${actualName}@${version}`,
resolution: {
type: 'custom:cdn',
cdnUrl: `https://cdn.company.com/packages/${actualName}/${version}.tgz`,
cachedAt: Date.now(), // Custom metadata for shouldRefreshResolution
},
}
},
shouldRefreshResolution: (depPath, pkgSnapshot) => {
// Check custom metadata stored in the resolution
const cachedAt = pkgSnapshot.resolution?.cachedAt
if (cachedAt && Date.now() - cachedAt > 24 * 60 * 60 * 1000) {
return true // Re-resolve if cached more than 24 hours ago
}
return false
},
}
const customFetcher = {
canFetch: (pkgId, resolution) => {
return resolution.type === 'custom:cdn'
},
fetch: async (cafs, resolution, opts, fetchers) => {
// Delegate to pnpm's standard tarball fetcher
// Transform the custom resolution to a standard tarball resolution
const tarballResolution = {
tarball: resolution.cdnUrl,
integrity: resolution.integrity,
}
return fetchers.remoteTarball(cafs, tarballResolution, opts)
},
}
// Export as top-level arrays
module.exports = {
resolvers: [customResolver],
fetchers: [customFetcher],
}
Delegating to Standard Fetchers:
The fetchers parameter passed to the custom fetcher's fetch method provides access to pnpm's standard fetchers for delegation:
fetchers.remoteTarball- Fetch from remote tarball URLsfetchers.localTarball- Fetch from local tarball filesfetchers.gitHostedTarball- Fetch from GitHub/GitLab/Bitbucket tarballsfetchers.directory- Fetch from local directoriesfetchers.git- Fetch from git repositories
See the test cases in resolving/default-resolver/test/customResolver.ts and fetching/pick-fetcher/test/pickFetcher.ts for complete working examples.
Notes:
- Multiple custom resolvers and fetchers can be registered; they are tried in order until one matches
- All methods support both synchronous and asynchronous implementations
- Custom resolvers are tried before pnpm's built-in resolvers (npm, git, tarball, etc.)
- Custom fetchers can delegate to pnpm's standard fetchers via the
fetchersparameter to avoid reimplementing common fetch logic - The
shouldRefreshResolutionhook allows fine-grained control over when packages should be re-resolved
Performance Considerations:
canResolve()should be a cheap check (ideally synchronous) as it's called for every dependency during resolutionresolve()can be an expensive async operation (e.g., network requests) as it's only called for matching dependencies- If your
canResolve()implementation is expensive, performance may be impacted during large installations