.pnpmfile.mjs
pnpm 允许你通过特殊函数(hooks)直接钩子到安装过程。Hooks 可以在名为 .pnpmfile.mjs(ESM)或 .pnpmfile.cjs(CommonJS)的文件中声明。
🌐 pnpm lets you hook directly into the installation process via special functions
(hooks). Hooks can be declared in a file called .pnpmfile.mjs (ESM) or .pnpmfile.cjs (CommonJS).
默认情况下,.pnpmfile.mjs 应位于与锁文件相同的目录中。例如,在具有共享锁文件的 工作区 中,.pnpmfile.mjs 应位于 monorepo 的根目录下。
🌐 By default, .pnpmfile.mjs should be located in the same directory as the
lockfile. For instance, in a workspace with a shared lockfile,
.pnpmfile.mjs should be in the root of the monorepo.
钩子
🌐 Hooks
长话短说
🌐 TL;DR
| 钩子函数 | 过程 | 用途 |
|---|---|---|
hooks.readPackage(pkg, context): pkg | 在 pnpm 解析依赖的 package 清单后调用 | 允许你修改依赖的 package.json。 |
hooks.afterAllResolved(lockfile, context): lockfile | 在依赖解析完成后调用 | 允许你修改锁文件。 |
hooks.beforePacking(pkg): pkg | 在打包/发布期间创建 tar 包之前调用 | 允许你自定义发布的 package.json |
resolvers | 在包解析期间调用 | 允许你注册自定义的包解析器。 |
fetchers | 在包获取期间调用 | 允许你注册自定义的包获取器。 |
hooks.readPackage(pkg, context): pkg | Promise<pkg>
允许你在解析之后、解析前更改依赖的 package.json。这些更改不会保存到文件系统,但它们会影响锁文件中解析的内容,从而影响安装的内容。
🌐 Allows you to mutate a dependency's package.json after parsing and prior to
resolution. These mutations are not saved to the filesystem, however, they will
affect what gets resolved in the lockfile and therefore what gets installed.
请注意,如果你已经解决了想要修改的依赖,则需要删除 pnpm-lock.yaml。
🌐 Note that you will need to delete the pnpm-lock.yaml if you have already
resolved the dependency you want to modify.
如果你需要将对 package.json 的更改保存到文件系统中,你需要使用 pnpm patch 命令并修改 package.json 文件。例如,如果你想移除某个依赖的 bin 字段,这可能会很有用。
🌐 If you need changes to package.json saved to the filesystem, you need to use the pnpm patch command and patch the package.json file.
This might be useful if you want to remove the bin field of a dependency for instance.
参数
🌐 Arguments
pkg- 软件包的清单。可以是来自注册表的响应,或者是package.json内容。context- 步骤的上下文对象。方法#log(msg)允许你为该步骤使用调试日志。
用法
🌐 Usage
示例 .pnpmfile.mjs(更改依赖的依赖):
🌐 Example .pnpmfile.mjs (changes the dependencies of a dependency):
function readPackage(pkg, context) {
// Override the manifest of foo@1.x after downloading it from the registry
if (pkg.name === 'foo' && pkg.version.startsWith('1.')) {
// Replace bar@x.x.x with bar@2.0.0
pkg.dependencies = {
...pkg.dependencies,
bar: '^2.0.0'
}
context.log('bar@1 => bar@2 in dependencies of foo')
}
// This will change any packages using baz@x.x.x to use baz@1.2.3
if (pkg.dependencies.baz) {
pkg.dependencies.baz = '1.2.3';
}
return pkg
}
export const hooks = {
readPackage
}
已知的限制
🌐 Known limitations
通过 readPackage 从依赖的清单中移除 scripts 字段并不会阻止 pnpm 构建该依赖。在构建依赖时,pnpm 会从包的归档中读取包的 package.json,这不会受到钩子的影响。若要忽略某个包的构建,请使用 allowBuilds 字段。
🌐 Removing the scripts field from a dependency's manifest via readPackage will
not prevent pnpm from building the dependency. When building a dependency, pnpm
reads the package.json of the package from the package's archive, which is not
affected by the hook. In order to ignore a package's build, use the
allowBuilds field.
hooks.updateConfig(config): config | Promise<config>
新增于:v10.8.0
🌐 Added in: v10.8.0
允许你修改 pnpm 使用的配置设置。当与 configDependencies 配合使用时,此钩子最为有用,它可以让你在不同的 Git 仓库之间共享和重用设置。
🌐 Allows you to modify the configuration settings used by pnpm. This hook is most useful when paired with configDependencies, allowing you to share and reuse settings across different Git repositories.
例如,@pnpm/plugin-better-defaults 使用 updateConfig 钩子来应用一组精心挑选的推荐设置。
🌐 For example, @pnpm/plugin-better-defaults uses the updateConfig hook to apply a curated set of recommended settings.
使用示例
🌐 Usage example
export const hooks = {
updateConfig (config) {
return Object.assign(config, {
enablePrePostScripts: false,
optimisticRepeatInstall: true,
resolutionMode: 'lowest-direct',
verifyDepsBeforeRun: 'install',
})
}
}
hooks.afterAllResolved(lockfile, context): lockfile | Promise<lockfile>
允许你在序列化之前更改锁定文件输出。
🌐 Allows you to mutate the lockfile output before it is serialized.
参数
🌐 Arguments
lockfile- 序列化到pnpm-lock.yaml的锁文件解析对象。context- 步骤的上下文对象。方法#log(msg)允许你为该步骤使用调试日志。
使用示例
🌐 Usage example
function afterAllResolved(lockfile, context) {
// ...
return lockfile
}
export const hooks = {
afterAllResolved
}
已知限制
🌐 Known Limitations
没有——任何可以通过锁文件完成的操作都可以通过这个函数修改,你甚至可以扩展锁文件的功能。
🌐 There are none - anything that can be done with the lockfile can be modified via this function, and you can even extend the lockfile's functionality.
hooks.beforePacking(pkg): pkg | Promise<pkg>
新增于:v10.28.0
🌐 Added in: v10.28.0
允许你在 pnpm pack 或 pnpm publish 期间将 package.json 清单打包为 tar 包之前进行修改。这对于在不影响本地开发的 package.json 的情况下自定义发布的包非常有用。
🌐 Allows you to modify the package.json manifest before it is packed into a tarball during pnpm pack or pnpm publish. This is useful for customizing the published package without affecting your local development package.json.
与在安装过程中修改依赖解析方式的 hooks.readPackage 不同,beforePacking 仅影响最终发布的压缩包的内容。
🌐 Unlike hooks.readPackage, which modifies how dependencies are resolved during installation, beforePacking only affects the contents of the tarball that gets published.
参数
🌐 Arguments
pkg- 将包含在发布的 tar 包中的包清单对象。
使用示例
🌐 Usage example
function beforePacking(pkg) {
// Remove development-only fields from published package
delete pkg.devDependencies
delete pkg.scripts.test
// Add publication metadata
pkg.publishedAt = new Date().toISOString()
// Modify package exports for production
if (pkg.name === 'my-package') {
pkg.main = './dist/index.js'
}
return pkg
}
export const hooks = {
beforePacking
}
此钩子所做的修改仅影响压缩包内的 package.json。你本地的 package.json 文件保持不变。
🌐 The modifications made by this hook only affect the package.json inside the tarball. Your local package.json file remains unchanged.
hooks.preResolution(options): Promise<void>
此钩子在读取和解析项目的锁文件之后执行,但在解析依赖之前执行。它允许对锁文件对象进行修改。
🌐 This hook is executed after reading and parsing the lockfiles of the project, but before resolving dependencies. It allows modifications to the lockfile objects.
参数
🌐 Arguments
options.existsCurrentLockfile- 如果node_modules/.pnpm/lock.yaml位置的锁文件存在,则布尔值为 true。options.currentLockfile-node_modules/.pnpm/lock.yaml的锁文件对象。options.existsNonEmptyWantedLockfile- 如果pnpm-lock.yaml位置的锁文件存在,则布尔值为 true。options.wantedLockfile-pnpm-lock.yaml的锁文件对象。options.lockfileDir- 所需锁文件所在的目录。options.storeDir- 存储目录的位置。options.registries- 范围到注册表 URL 的映射。
hooks.importPackage(destinationDir, options): Promise<string | undefined>
这个钩子允许更改如何将包写入 node_modules。返回值是可选的,用于说明用于导入依赖的方法,例如:克隆、硬链接。
🌐 This hook allows to change how packages are written to node_modules. The return value is optional and states what method was used for importing the dependency, e.g.: clone, hardlink.
参数
🌐 Arguments
destinationDir- 包写入的目标目录。options.disableRelinkLocalDirDepsoptions.filesMapoptions.forceoptions.resolvedFromoptions.keepModulesDir
hooks.fetchers
hooks.fetchers 已被移除。请改用顶层 fetchers。有关新 API,请参阅 自定义获取器 部分。
查找器
🌐 Finders
新增于:v10.16.0
🌐 Added in: v10.16.0
查找器功能通过 --find-by 标志与 pnpm list 和 pnpm why 一起使用。
🌐 Finder functions are used with pnpm list and pnpm why via the --find-by flag.
示例:
🌐 Example:
export const finders = {
react17: (ctx) => {
return ctx.readManifest().peerDependencies?.react === "^17.0.0"
}
}
用法:
🌐 Usage:
pnpm why --find-by=react17
详情请参阅[查找器]。
🌐 See Finders for more details.
自定义解析器和获取器
🌐 Custom Resolvers and Fetchers
新增于:v11.0.0
🌐 Added in: v11.0.0
自定义解析器和获取器允许你为新的包标识符方案(如 my-protocol:package-name)实现自定义的包解析和获取逻辑。它们在 .pnpmfile.cjs 中注册为顶层导出:
🌐 Custom resolvers and fetchers allow you to implement custom package resolution and fetching logic for new package identifier schemes (like my-protocol:package-name). They are registered as top-level exports in .pnpmfile.cjs:
module.exports = {
resolvers: [customResolver1, customResolver2],
fetchers: [customFetcher1, customFetcher2],
}
TypeScript 接口
🌐 TypeScript Interfaces
interface CustomResolver {
canResolve?: (wantedDependency: WantedDependency) => boolean | Promise<boolean>
resolve?: (wantedDependency: WantedDependency, opts: ResolveOptions) => ResolveResult | Promise<ResolveResult>
shouldRefreshResolution?: (depPath: string, pkgSnapshot: PackageSnapshot) => boolean | Promise<boolean>
}
interface CustomFetcher {
canFetch?: (pkgId: string, resolution: Resolution) => boolean | Promise<boolean>
fetch?: (cafs: Cafs, resolution: Resolution, opts: FetchOptions, fetchers: Fetchers) => FetchResult | Promise<FetchResult>
}
自定义解析器
🌐 Custom Resolvers
自定义解析器将包描述符(例如 foo@^1.0.0)转换为存储在锁文件中的解析结果。
🌐 Custom resolvers convert package descriptors (e.g., foo@^1.0.0) into resolutions that are stored in the lockfile.
解析器接口
🌐 Resolver Interface
自定义解析器是一个可以实现以下任意组合方法的对象:
🌐 A custom resolver is an object that can implement any combination of the following methods:
canResolve(wantedDependency): boolean | Promise<boolean>
确定此解析器是否能够解析给定的所需依赖。
🌐 Determines whether this resolver can resolve a given wanted dependency.
参数:
-
wantedDependency- Object with:alias- 在 package.json 中显示的包名称或别名bareSpecifier- 版本范围、git URL、文件路径或其他说明符
返回值: 如果此解析器可以处理该包,则返回 true,否则返回 false。这将决定是否调用 resolve。
resolve(wantedDependency, opts): ResolveResult | Promise<ResolveResult>
将所需的依赖解析为特定的包元数据和解析信息。
🌐 Resolves a wanted dependency to specific package metadata and resolution information.
参数:
-
wantedDependency- The wanted dependency (same ascanResolve) -
opts- Object with:lockfileDir- 包含锁文件的目录projectDir- 项目根目录preferredVersions- 包名到首选版本的映射
返回: 包含以下内容的对象:
-
id- Unique package identifier (e.g.,'custom-pkg@1.0.0') -
resolution- Resolution metadata. This can be:- 标准分辨率,例如
{ tarball: 'https://...', integrity: '...' } - 自定义分辨率:
{ type: 'custom:cdn', url: '...' }
- 标准分辨率,例如
自定义解析必须由相应的自定义获取器处理。
🌐 Custom resolutions must be handled by a corresponding custom fetcher.
自定义解析必须在其类型字段中使用 custom: 前缀(例如,custom:cdn、custom:artifactory),以将其与 pnpm 内置的解析类型区分开。
🌐 Custom resolutions must use the custom: prefix in their type field (e.g., custom:cdn, custom:artifactory) to differentiate them from pnpm's built-in resolution types.
shouldRefreshResolution(depPath, pkgSnapshot): boolean | Promise<boolean>
返回 true 以触发所有包的完全解析,跳过“锁文件是最新的”优化。这对于实现基于时间的缓存失效或其他自定义重新解析逻辑很有用。
🌐 Return true to trigger full resolution of all packages, skipping the "Lockfile is up to date" optimization. This is useful for implementing time-based cache invalidation or other custom re-resolution logic.
参数:
-
depPath- The package identifier string (e.g.,lodash@4.17.21) -
pkgSnapshot- The lockfile entry for this package, providing direct access to the resolution, dependencies, etc.
返回值: true 用于强制重新解析,否则返回 false。
shouldRefreshResolution 在冻结锁定文件安装期间会被跳过,因为在该模式下不允许进行解析。
自定义获取器
🌐 Custom Fetchers
自定义获取器完全处理自定义包类型的获取,从自定义来源下载包内容并将其存储在 pnpm 的内容寻址文件系统中。
🌐 Custom fetchers completely handle fetching for custom package types, downloading package contents from custom sources and storing them in pnpm's content-addressable file system.
获取器接口
🌐 Fetcher Interface
自定义获取器是一个可以实现以下方法的对象:
🌐 A custom fetcher is an object that can implement the following methods:
canFetch(pkgId, resolution): boolean | Promise<boolean>
确定此获取器是否可以获取具有给定解析的包。
🌐 Determines whether this fetcher can fetch a package with the given resolution.
参数:
-
pkgId- The unique package identifier from the resolution phase -
resolution- The resolution object from a resolver'sresolvemethod
返回值: 如果此获取器可以处理获取此软件包,则返回 true,否则返回 false。
fetch(cafs, resolution, opts, fetchers): FetchResult | Promise<FetchResult>
获取软件包文件并返回有关获取的软件包的元数据。
🌐 Fetches package files and returns metadata about the fetched package.
参数:
-
cafs- Content-addressable file system interface for storing files -
resolution- The resolution object (same as passed tocanFetch) -
opts- Fetch options including:lockfileDir- 包含锁文件的目录filesIndexFile- 文件索引的路径onStart- 获取开始时的可选回调onProgress- 可选进度回调
-
fetchers- Object containing pnpm's standard fetchers for delegation:remoteTarball- 远程 tar 包获取器localTarball- 本地 tar 包的获取器gitHostedTarball- 用于 GitHub/GitLab/Bitbucket 压缩包的获取器directory- 本地目录获取器git- Git 仓库获取器
返回: 包含以下内容的对象:
-
filesIndex- Map of relative file paths to their physical locations. For remote packages, these are paths in pnpm's content-addressable store (CAFS). For local packages (whenlocal: true), these are absolute paths to files on disk. -
manifest- Optional. The package.json from the fetched package. If not provided, pnpm will read it from disk when needed. Providing it avoids an extra file I/O operation and is recommended when you have the manifest data readily available (e.g., already parsed during fetch). -
requiresBuild- Boolean indicating whether the package has build scripts that need to be executed. Set totrueif the package haspreinstall,install, orpostinstallscripts, or containsbinding.gypor.hooks/files. Standard fetchers determine this automatically using the manifest and file list. -
local- Optional. Set totrueto load the package directly from disk without copying to pnpm's store. Whentrue,filesIndexshould contain absolute paths to files on disk, and pnpm will hardlink them tonode_modulesinstead of copying. This is how the directory fetcher handles local dependencies (e.g.,file:../my-package).
自定义抓取器可以使用 fetchers 参数委托给 pnpm 的内置抓取器。
🌐 Custom fetchers can delegate to pnpm's built-in fetchers using the fetchers parameter.
使用示例
🌐 Usage Examples
基本自定义解析器
🌐 Basic Custom Resolver
此示例显示了一个从自定义注册表解析软件包的自定义解析器:
🌐 This example shows a custom resolver that resolves packages from a custom registry:
const customResolver = {
// Only handle packages with @company scope
canResolve: (wantedDependency) => {
return wantedDependency.alias.startsWith('@company/')
},
resolve: async (wantedDependency, opts) => {
// Fetch metadata from custom registry
const response = await fetch(
`https://custom-registry.company.com/${wantedDependency.alias}/${wantedDependency.bareSpecifier}`
)
const metadata = await response.json()
return {
id: `${metadata.name}@${metadata.version}`,
resolution: {
tarball: metadata.tarballUrl,
integrity: metadata.integrity
}
}
}
}
module.exports = {
resolvers: [customResolver]
}
使用 shouldRefreshResolution 的自定义解析器和获取器
🌐 Custom Resolver and Fetcher with shouldRefreshResolution
这个例子展示了一个解析器和获取器如何与自定义解析类型及基于时间的缓存失效机制一起工作:
🌐 This example shows a resolver and fetcher working together with a custom resolution type and time-based cache invalidation:
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
const tarballResolution = {
tarball: resolution.cdnUrl,
integrity: resolution.integrity,
}
return fetchers.remoteTarball(cafs, tarballResolution, opts)
},
}
module.exports = {
resolvers: [customResolver],
fetchers: [customFetcher],
}
基础自定义获取器
🌐 Basic Custom Fetcher
此示例显示了一个自定义获取器,它从不同的源获取某些软件包:
🌐 This example shows a custom fetcher that fetches certain packages from a different source:
const customFetcher = {
canFetch: (pkgId, resolution) => {
return pkgId.startsWith('@company/')
},
fetch: async (cafs, resolution, opts, fetchers) => {
// Delegate to pnpm's tarball fetcher with modified URL
const tarballResolution = {
tarball: resolution.tarball.replace(
'https://registry.npmjs.org/',
'https://custom-registry.company.com/'
),
integrity: resolution.integrity
}
return fetchers.remoteTarball(cafs, tarballResolution, opts)
}
}
module.exports = {
fetchers: [customFetcher]
}
带解析器和获取器的自定义解析类型
🌐 Custom Resolution Type with Resolver and Fetcher
此示例显示了自定义解析器和获取器如何与自定义解析类型一起工作:
🌐 This example shows a custom resolver and fetcher working together with a custom resolution type:
const customResolver = {
canResolve: (wantedDependency) => {
return wantedDependency.alias.startsWith('@internal/')
},
resolve: async (wantedDependency) => {
return {
id: `${wantedDependency.alias}@${wantedDependency.bareSpecifier}`,
resolution: {
type: 'custom:internal-directory',
directory: `/packages/${wantedDependency.alias}/${wantedDependency.bareSpecifier}`
}
}
}
}
const customFetcher = {
canFetch: (pkgId, resolution) => {
return resolution.type === 'custom:internal-directory'
},
fetch: async (cafs, resolution, opts, fetchers) => {
// Delegate to pnpm's directory fetcher for local packages
const directoryResolution = {
type: 'directory',
directory: resolution.directory
}
return fetchers.directory(cafs, directoryResolution, opts)
}
}
module.exports = {
resolvers: [customResolver],
fetchers: [customFetcher]
}
优先级和排序
🌐 Priority and Ordering
当注册了多个解析器时,它们会按顺序进行检查。第一个 canResolve 返回 true 的解析器将被用于解析。同样适用于获取器:在获取阶段,第一个 canFetch 返回 true 的获取器将被使用。
🌐 When multiple resolvers are registered, they are checked in order. The first resolver where canResolve returns true will be used for resolution. The same applies for fetchers: The first fetcher where canFetch returns true will be used during the fetch phase.
自定义解析器会在 pnpm 内置的解析器(npm、git、tar 包等)之前尝试,这让你可以完全控制包的解析。
🌐 Custom resolvers are tried before pnpm's built-in resolvers (npm, git, tarball, etc.), giving you full control over package resolution.
性能考虑
🌐 Performance Considerations
canResolve()、canFetch() 和 shouldRefreshResolution() 应该是廉价检查(理想情况下是同步的),因为它们在解析每个依赖时都会被调用。
相关配置
🌐 Related Configuration
ignorePnpmfile
- 默认:假
- 类型:布尔
pnpmfile 将被忽略。当你想确保在安装过程中没有脚本被执行时,与 --ignore-scripts 一起使用非常有用。
🌐 The pnpmfile will be ignored. Useful together with --ignore-scripts when you
want to make sure that no script gets executed during install.
pnpmfile
- 默认: ['.pnpmfile.mjs']
- 类型:path[]
- 示例:['.pnpm/.pnpmfile.mjs']
本地 pnpm 文件的位置。
🌐 The location of the local pnpmfile(s).
globalPnpmfile
- 默认:空
- 类型:路径
- 示例:~/.pnpm/global_pnpmfile.mjs
全局 pnpmfile 的位置。全局 pnpmfile 在安装过程中被所有项目使用。
🌐 The location of a global pnpmfile. A global pnpmfile is used by all projects during installation.
建议使用本地 pnpmfiles。只有在项目不以 pnpm 作为主要包管理器时,才使用全局 pnpmfile。
🌐 It is recommended to use local pnpmfiles. Only use a global pnpmfile if you use pnpm on projects that don't use pnpm as the primary package manager.