.pnpmfile.mjs
pnpm 允许你通过特殊功能(钩子)直接挂钩到安装过程。钩子可以在名为 .pnpmfile.mjs (ESM) 或 .pnpmfile.cjs (CommonJS) 的文件中声明。
默认情况下, .pnpmfile.mjs 应该与锁文件位于同一目录中。例如,在具有共享锁文件的 工作区 中 .pnpmfile.mjs 应该位于 monorepo 的根目录中。
钩子#
摘要#
| 钩子函数 | 过程 | 用途 |
|---|---|---|
hooks.readPackage(pkg, context): pkg | 在 pnpm 解析依赖的软件清单文件后调用 | 允许你改变依赖的 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。这些突变不会保存到文件系统,但是,它们将影响被解析至锁文件的内容,从而影响哪些包将被安装。
请注意,如果你已经解析好了要修改的依赖项,则需要删除 pnpm-lock.yaml。
提示
如果你需要将 package.json 的变化保存到文件系统中,你需要使用 pnpm patch 命令对 package.json 文件进行修补。例如,如果你想删除依赖项的 bin 字段,这可能很有用。
参数#
pkg- 包的清单。来自注册源的响应或package.json的内容。context- 步骤的上下文对象。#log(msg)方法允许你使用该步骤的调试日志。
使用方法#
示例 .pnpmfile.mjs (更改依赖项的依赖关系):
function readPackage(pkg, context) {
// 在从源下载后覆盖 foo@1.x 的清单文件
if (pkg.name === 'foo' && pkg.version.startsWith('1.')) {
// 替换 bar@x.x.x 为 bar@2.0.0
pkg.dependencies = {
...pkg.dependencies,
bar: '^2.0.0'
}
context.log('bar@1 => bar@2 in dependencies of foo')
}
// 这将会将所有使用 baz@x.x.x 的包改为使用 baz@1.2.3
if (pkg.dependencies.baz) {
pkg.dependencies.baz = '1.2.3';
}
return pkg
}
module.exports = {
hooks: {
readPackage
}
}
已知限制#
使用 readPackage 从依赖项的清单中删除 scripts 字段不会阻止 pnpm 构建依赖项。构建依赖时,pnpm 从包的存档中读取包的 package.json ,这不受钩子的影响。 In order to ignore a package's build, use the allowBuilds field.
hooks.updateConfig(config): config | Promise<config>#
添加于:v10.8.0
允许你修改 pnpm 使用的配置设置。这个钩子与 configDependencies配对时非常有用,允许您在不同的 Git 存储库之间共享和重用设置。
例如, @pnpm/plugin-better-defaults 使用 updateConfig 钩子来应用一组精选的推荐设置。
用法示例#
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>#
允许你在序列化之前更改锁文件的输出。
参数#
lockfile- 锁文件的解析对象,序列化为pnpm-lock.yaml。context- 步骤的上下文对象。#log(msg)方法允许你使用该步骤的调试日志。
用法示例#
function afterAllResolved(lockfile, context) {
// ...
return lockfile
}
export const hooks = {
afterAllResolved
}
已知限制#
没有 - 任何可以通过修改锁文件达到的功能都可以通过这个函数完成,甚至可以扩展锁文件的功能。
hooks.beforePacking(pkg): pkg | Promise<pkg>#
新增于:v10.28.0
允许你在 pnpm pack 或 pnpm publish 过程中在被打包进 tarball 之前修改 package.json 。这有助于自定义已发布的软件包,而不影响你的本地开发的 package.json。
与 hooks.readPackage不同,后者会修改安装期间依赖项的解析方式,而 beforePacking 只会影响发布的 tarball 的内容。
参数#
pkg- 将包含在已发布的 tarball 中的软件包清单对象。
用法示例#
function beforePacking(pkg) {
// 从已发布的包中移除仅用于开发的字段
delete pkg.devDependencies
delete pkg.scripts.test
// 添加发布元数据
pkg.publishedAt = new Date().toISOString()
// 修改生产环境的包导出
if (pkg.name === 'my-package') {
pkg.main = './dist/index.js'
}
return pkg
}
export const hooks = {
beforePacking
}
注意
此钩子所做的修改只会影响 tarball 中的 package.json。你本地 package.json 文件保持不变。
hooks.preResolution(options): Promise<void>#
此钩子在读取和解析项目的锁文件 (lockfiles)之后但在解析依赖项之前执行。它允许修改锁文件对象。
参数#
options.existsCurrentLockfile- Boolean,如果node_modules/.pnpm/lock.yaml处的锁文件存在,则为true。options.currentLockfile- 来自node_modules/.pnpm/lock.yaml的 lockfile 对象。options.existsNonEmptyWantedLockfile- Boolean,如果pnpm-lock.yaml处的锁文件存在,则为true。options.wantedLockfile- 来自pnpm-lock.yaml的 lockfile 对象。options.lockfileDir- 所需锁文件所在的目录。options.storeDir- 存储目录的位置。options.registries- 范围到注册源 URL 的映射。
hooks.importPackage(destinationDir, options): Promise<string | undefined>#
此钩子允许更改将包写入 node_modules的方式。返回值是可选的,并说明用于导入依赖项的方法,例如:clone、hardlink。
参数#
destinationDir- 应该写入包的目标目录。options.disableRelinkLocalDirDepsoptions.filesMapoptions.forceoptions.resolvedFromoptions.keepModulesDir
hooks.fetchers#
已在 v11.0.0 版本中移除
hooks.fetchers 已被移除。请改用顶级 fetchers。有关新 API,请参阅 自定义获取器 部分。
查找器#
添加于:v10.16.0
查找器函数通过 --find-by 标志与 pnpm list 和 pnpm why 一起使用。
示例:
export const finders = {
react17: (ctx) => {
return ctx.readManifest().peerDependencies?.react === "^17.0.0"
}
}
使用方法:
pnpm why --find-by=react17
有关更多详细信息,请参阅 查找器 。
自定义解析器和获取器#
添加于:v11.0.0
自定义解析器和获取器允许你为新的包标识符方案(例如 my-protocol:package-name)实现自定义包解析和获取逻辑。它们在 .pnpmfile.cjs 中注册为顶级导出项:
module.exports = {
resolvers: [customResolver1, customResolver2],
fetchers: [customFetcher1, customFetcher2],
}
TypeScript 接口#
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>
}
自定义解析器#
自定义解析器将包描述符(例如, foo@^1.0.0)转换为存储在锁定文件中的解析结果。
解析器接口#
自定义解析器是一个可以实现以下方法的任意组合的对象:
canResolve(wantedDependency): boolean | Promise<boolean>#
确定该解析器是否能解决给定的依赖。
参数:
wantedDependency- 对象包含:alias- package.json 中显示的包名称或别名bareSpecifier- 版本范围、git URL、文件路径或其他说明符
返回值: 如果此解析器可以处理该包,则true ,否则 false 。这决定 resolve 是否被调用。
resolve(wantedDependency, opts): ResolveResult | Promise<ResolveResult>#
将所需的依赖关系解析为特定的软件包元数据和解析信息。
参数:
wantedDependency- 所需的依赖项(与canResolve相同)opts- 对象包含:lockfileDir- 包含锁定文件的目录projectDir- 项目根目录preferredVersions- 软件包名称到首选版本的映射
返回值: 对象,包含:
id- 唯一包标识符(例如,'custom-pkg@1.0.0')resolution- 解析元数据。这可以是:- 标准解析,例如
{ tarball: 'https://...', integrity: '...' } - 自定义解析:
{ type: 'custom:cdn', url: '...' }
- 标准解析,例如
自定义解析必须由相应的自定义获取器处理。
自定义解析类型
自定义解析必须在其类型字段中使用 custom: 前缀(例如, custom:cdn, custom:artifactory),以区别于 pnpm 的内置解析类型。
shouldRefreshResolution(depPath, pkgSnapshot): boolean | Promise<boolean>#
返回 true 以触发所有软件包的完整解析,跳过 “Lockfile is up to date” 优化。这对于实现基于时间的缓存失效或其他自定义重新解析逻辑非常有用。
参数:
depPath- 包标识符字符串(例如,lodash@4.17.21)pkgSnapshot- 此软件包的锁定文件条目,提供对解析、依赖项等的直接访问。
返回值: true 以强制重新解析, 否则false。
注意
shouldRefreshResolution 在冻结的锁文件安装过程中被跳过,因为在该模式下不允许分辨率。
自定义获取器#
自定义获取器完全处理自定义包类型的获取,从自定义源下载包内容并将其存储在 pnpm 的内容寻址文件系统中。
获取器接口#
自定义获取器是一个可以实现以下方法的对象:
canFetch(pkgId, resolution): boolean | Promise<boolean>#
确定此获取器是否可以获取具有给定解析的软件包。
参数:
pkgId- 解析阶段的唯一软件包标识符resolution- 来自解析器resolve方法的解析对象
返回值: 如果此解析器可以处理该包的获取,则 true ,否则 false 。
fetch(cafs, resolution, opts, fetchers): FetchResult | CustomFetcherDelegation | Promise<FetchResult | CustomFetcherDelegation>#
获取软件包文件并返回有关所获取软件包的元数据。
参数:
cafs- 用于存储文件的内容寻址文件系统接口resolution- 解析对象(与传递给canFetch的对象相同)opts- 获取选项,包括:lockfileDir- 包含锁定文件的目录filesIndexFile- 文件索引路径onStart- 获取开始时的可选回调onProgress- 可选的进度回调
获取器- 包含 pnpm 标准委托获取器的对象:remoteTarball- 远程 tar 包获取器localTarball- 本地 tar 包获取器gitHostedTarball- GitHub/GitLab/Bitbucket tarballs 获取器directory- 本地目录获取器git- git 仓库获取器
**返回值:**获取结果或委托封装对象。
获取结果是一个包含以下属性的对象:
filesIndex- 文件相对路径到实际位置的映射。对于远程包,这些是 pnpm 内容寻址存储 (CAFS) 中的路径。对于本地包(当local: true时),这些是磁盘上文件的绝对路径。manifest- 可选。已获取包的 package.json 。如果未提供,pnpm 将在需要时从磁盘读取。提供它可以避免额外的文件 I/O 操作,并且建议当你有便于获取的清单数据时(例如在获取过程中已经解析)。requiresBuild- 布尔值,指示软件包是否具有需要执行的构建脚本。如果软件包有preinstallinstall, 或postinstall脚本, 或包含binding.gyp或.hooks/文件时设置为true。标准获取器会使用清单和文件列表自动确定这一点。local- 可选。设置为true可直接从磁盘加载包,而无需复制到 pnpm 的存储中。当true时,filesIndex应包含磁盘上文件的绝对路径,并且 pnpm 会将它们硬链接到node_modules而不是复制。这就是目录获取器处理本地依赖项的方式(例如,file:../my-package)。
委托给内置获取器#
添加于:v11.12.0
自定义获取器可以选择不获取包,而是将这项工作交回给 pnpm 处理。有两种方法可以做到这一点。
返回一个 { delegate } 封装对象。 不返回常规的获取结果,而是返回一个仅包含 delegate 键的对象,该键的值指定了 pnpm 应当改为获取的目标。 pnpm 会将该包的解析结果重写为那种格式,并对其执行内置的获取流程:
const customFetcher = {
canFetch: (pkgId, resolution) => resolution.type === 'custom:url',
fetch: (cafs, resolution) => ({
delegate: {
tarball: resolution.customUrl,
integrity: resolution.integrity,
},
}),
}
module.exports = { fetchers: [customFetcher] }
经委派的解析结果必须是一个完整且可获取的形式(例如 { tarball, integrity })。委托是单步的:如果 delegate 本身具有自定义类型,则会被拒绝。
直接调用 fetchers.*。 fetchers 参数提供了 pnpm 的标准获取器,因此你可以转换解析结果并自行调用其中一个。
建议使用委托对象,以便于移植
{ delegate } 封装是唯一一种既适用于 pnpm 又适用于 pacquet(pnpm 的 Rust 移植版本)的委托形式。 pacquet 通过 IPC 调用 pnpmfile 获取器,其中 cafs 和 fetchers 不存在,并且两者都取得 null。旨在任一技术栈上运行的获取器必须返回“委托对象”,而不是调用 fetchers.*。
使用示例#
基本自定义解析器#
此示例展示了一个自定义解析器,用于解析来自自定义注册表的软件包:
const customResolver = {
// 仅处理带有 @company 作用域的包
canResolve: (wantedDependency) => {
return wantedDependency.alias.startsWith('@company/')
},
resolve: async (wantedDependency, opts) => {
// 从自定义注册源中获取元数据
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#
此示例展示了解析器和获取器如何协同工作,使用自定义解析类型和基于时间的缓存失效机制:
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)
返回 {
id: `company-cdn:${actualName}@${version}`,
resolution: {
type: 'custom:cdn',
cdnUrl: `https://cdn.company.com/packages/${actualName}/${version}.tgz`,
cachedAt: Date.now(), // shouldRefreshResolution 的自定义元数据
},
}
},
shouldRefreshResolution: (depPath, pkgSnapshot) => {
// 检查存储在分辨率中的自定义元数据
const cachedAt = pkgSnapshot.resolution?.cachedAt
if (cachedAt && Date.now() - cachedAt > 24 * 60 * 60 * 1000) {
return true // 如果缓存时间超过 24 小时,则重新解析
}
return false
},
}
const customFetcher = {
canFetch: (pkgId, resolution) => {
return resolution.type === 'custom:cdn'
},
fetch: async (cafs, resolution, opts, fetchers) => {
// 委托给 pnpm 的标准 tarball 获取器
const tarballResolution = {
tarball: resolution.cdnUrl,
integrity: resolution.integrity,
}
return fetchers.remoteTarball(cafs, tarballResolution, opts)
},
}
module.exports = {
resolvers: [customResolver],
fetchers: [customFetcher],
}
基本自定义获取器#
此示例展示了一个自定义获取器,它可以从不同的来源获取某些软件包:
const customFetcher = {
canFetch: (pkgId, resolution) => {
return pkgId.startsWith('@company/')
},
fetch: async (cafs, resolution, opts, fetchers) => {
// 使用修改后的 URL 委托给 pnpm 的 tarball 获取器
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]
}
带有解析器和获取器的自定义解析类型#
此示例展示了自定义解析器和获取器如何与自定义解析类型协同工作:
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]
}
优先级和排序#
当注册多个解析器时,它们会按顺序进行检查。第一个 canResolve 返回 true 的解析器将用于解析。这也适用于获取器:第一个 canFetch 返回 true 的获取器将在获取阶段使用。
自定义解析器会优先于 pnpm 的内置解析器(npm、git、tarball 等)进行尝试,让你可以完全控制包解析。
性能考量#
canResolve(), canFetch(), 和 shouldRefreshResolution() 应该是开销很小的检查(理想情况下是同步的),因为它们在解析期间会为每个依赖项调用。
相关配置#
ignorePnpmfile#
- 默认值:false
- 类型:Boolean
pnpmfile 将被忽略。想要确保在安装期间没有执行任何脚本的话,和 --ignore-scripts 一起使用会比较方便。
pnpmfile#
- 默认值: ['.pnpmfile.mjs']
- 类型: 路径[]
- 例子:['.pnpm/.pnpmfile.mjs']
本地 pnpmfile(s) 的位置。
globalPnpmfile#
- 默认值:null
- 类型:path
- 例子:~/.pnpm/global_pnpmfile.mjs
全局 pnpmfile 的位置。在安装期间,所有项目都会使用全局 pnpmfile 。
注意
建议使用本地 pnpmfiles。仅当在未使用 pnpm 作为主包管理器的项目上使用 pnpm 时,才使用全局 pnpmfile 。