pnpm 按注册表声明time字段:让resolutionMode: time-based的完整元数据回退按需生效
【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm
导读
pnpm 新增了按注册表(per-registry)声明简略元数据(abbreviated metadata)是否携带time字段的能力:在pnpm-workspace.yaml的registries中为某个注册表声明supportsTimeField: true后,resolutionMode: time-based只会从确实缺少time字段的注册表拉取体积大得多的完整元数据文档,其余注册表继续使用简略文档。读完本文你将掌握该配置的完整写法、新旧行为差异、优先级规则,以及它在 pnpm 源码(TypeScript 与 Rust 双实现)中从配置解析到解析器决策的完整调用链。
背景:时间基解析与time字段
pnpm 在resolutionMode: time-based(以及minimumReleaseAge等发布时间门控策略)下,需要知道每个包版本的实际发布时间。这个时间来自注册表元数据文档中的time字段——一个从版本号映射到 ISO 时间戳的对象。
问题在于,npm 公共注册表registry.npmjs.org的简略元数据(packument 的缩写形式)不包含time字段。为了拿到发布时间,pnpm 只能回退去拉取包含全部信息的完整元数据文档,而后者体积远大于简略版,会显著拖慢解析并消耗更多带宽与存储。
旧行为:全有或全无的全局开关
在本次改动之前,是否因为时间基解析而拉取完整元数据,由一个全局布尔配置registrySupportsTimeField统一回答,对每一个注册表一视同仁:
- 不设置它(默认
false):项目只要开启时间基解析,所有注册表都要回退到完整元数据; - 设置为
true:则假定所有注册表的简略元数据都带time字段,可registry.npmjs.org根本不提供,解析结果就会缺少time而失败。
正如变更记录(.changeset/per-registry-time-field.md)所总结的:一个同时从公共注册表和 Verdaccio 私有实例解析依赖的项目,要么为所有注册表付出完整元数据的代价,要么声称 npmjs 提供了它并不提供的time字段——两种选择都不合理。
新能力:按注册表声明supportsTimeField
现在,注册表可以声明它的简略元数据携带time字段,这样resolutionMode: time-based只会从真正需要它的注册表读取完整元数据:
resolutionMode: time-based registries: https://npm.internal.example/: supportsTimeField: true- 声明了
supportsTimeField: true的注册表(如上例的内部注册表):时间基解析继续走轻量的简略元数据; - 未声明的注册表(如
registry.npmjs.org):保持旧逻辑,按全局registrySupportsTimeField设置回答,需要时回退到完整元数据。
关键语义:registrySupportsTimeField仍然是"未声明注册表"的答案——声明只对声明者本身生效,不会豁免其他注册表。
配置详解与优先级
字段位置与校验
该声明位于pnpm-workspace.yaml的registries映射下,作为每个注册表 URL 的对象字段。配置读取端在 getOptionsFromRootManifest.ts 中把supportsTimeField列入注册表声明字段集合:
const REGISTRY_DECLARATION_FIELDS = new Set(['serverType', 'supportsTimeField', 'scopes', 'prefix'])并且与serverType等字段一样做布尔断言校验,非布尔值会直接报错:
if (supportsTimeField != null) { assertBoolean(supportsTimeField, `${settingPath}.supportsTimeField`) }在 Rust 实现侧,该字段同样被解析进注册表选项结构体,见 registry_options.rs 与工作区 YAML 解析的 ecosystems.rs。
优先级:声明 > 全局设置 > 默认 false
答案的判定逻辑集中在 normalize-registries/src/index.ts 的registrySupportsTimeField函数:
export function registrySupportsTimeField ( registryContext: Pick<RegistryContext, 'registryOptionsByUrl'> & { registrySupportsTimeField?: boolean }, registry: string ): boolean { return registryContext.registryOptionsByUrl?.[normalizeRegistryUrl(registry)]?.supportsTimeField ?? registryContext.registrySupportsTimeField ?? false }三层回退一目了然:
- 该注册表 URL 在
registryOptionsByUrl中的supportsTimeField声明(按规范化后的 URL 匹配); - 全局
registrySupportsTimeField设置; - 默认
false。
注意第 1 层用normalizeRegistryUrl(registry)做键匹配,因此声明时 URL 尾部有没有斜杠都不影响命中(对应测试见后文 shouldFetchFullMetadata.test.ts)。
声明覆盖全局设置(双向生效)
测试 shouldFetchFullMetadata.test.ts 验证了声明与全局设置的关系是"双向覆盖":
- 全局设置
registrySupportsTimeField: true,但某注册表显式声明supportsTimeField: false→ 该注册表仍需完整元数据(声明优先); - 未声明注册表 → 遵循全局设置。
从配置到解析:内部实现调用链
1. 存储控制器:按注册表回答"是否需要完整元数据"
核心决策函数位于 createNewStoreController.ts。首先是一个无歧义的全局策略函数:
function fullMetadataPolicy (opts: FullMetadataPolicyOptions, supportsTimeField: boolean): boolean { return opts.fetchFullMetadata ?? ( opts.supportedArchitectures?.libc != null || opts.trustPolicy === 'no-downgrade' || (opts.resolutionMode === 'time-based' && !supportsTimeField) ) }注意完整元数据的需求来源不止时间基解析,还包括:显式fetchFullMetadata开关、supportedArchitectures.libc配置(npm 简略元数据不含libc)、trustPolicy: no-downgrade(信任检查需要读取简略元数据永远不携带的_npmUser信任证据)。这三个理由适用于所有注册表,不因任何声明而豁免。
needsFullMetadataForRegistry则把同一策略逐注册表问一遍,并用Map做了记忆化(每个注册表只算一次):
export function needsFullMetadataForRegistry ( opts: FullMetadataPolicyOptions ): (registry: string) => boolean { const answers = new Map<string, boolean>() return (registry: string): boolean => { let answer = answers.get(registry) if (answer == null) { answer = fullMetadataPolicy(opts, registrySupportsTimeField(opts, registry)) answers.set(registry, answer) } return answer } }2. npm 解析器:把"逐注册表"能力接入
解析器工厂在 npm-resolver/src/index.ts 中新增了一个回调选项needsFullMetadataFor:
/** * Asked instead of {@link ResolverFactoryOptions.fullMetadata} when the * caller can answer per registry — a registry that declares * `supportsTimeField` needs no full metadata for a time-based resolution * even when the others do. */ needsFullMetadataFor?: (registry: string) => boolean包选择阶段(pickPackage.ts)按当前包的注册表 URL 决定拉取哪种文档:
const policyWantsFullMetadata = ctx.needsFullMetadataFor?.(opts.registry) ?? ctx.fullMetadata === true const fullMetadata = opts.optional === true || policyWantsFullMetadata此外,元数据缓存键也把fullMetadata/filterMetadata纳入其中(见getPkgMetaCacheKey),确保同一注册表的简略与完整文档镜像各自独立缓存,互不污染。
3. 过滤镜像的一致性:按最苛刻的注册表决定
有一个容易被忽视的细节:完整 packument 一旦被拉取,会以 pnpm 过滤后的形式存储与读取(剥离time等用不到的大字段)。由于这个"过滤镜像"是全客户端选一次的,shouldFilterMetadata必须按最苛刻的注册表回答——即假设supportsTimeField为false时的策略结果(见 createNewStoreController.ts)。测试 shouldFetchFullMetadata.test.ts 专门验证了这种"全局豁免但声明不豁免"的场景:shouldFetchFullMetadata返回false,但needsFullMetadataForRegistry对该注册表返回true,此时shouldFilterMetadata必须为true,保证完整文档落入正确的过滤镜像。
声明如何同步到 pnpr 服务器
变更记录特别提到:该声明也会发送给 pnpr 服务器,由服务器在代表客户端执行的解析中应用同样的逐注册表逻辑。
这在 normalize-registries/src/index.ts 的toRegistryDeclarations中实现——它把配置读取时拆分成的查找表重新组装为声明结构,供客户端向 pnpr 服务器描述自己的注册表:
/** * Rebuilds the declarations from the lookups they were split into, for a * client that has to describe its registries to a pnpr server. */ export function toRegistryDeclarations (context: Partial<RegistryContext>): Record<string, RegistryDeclaration>组装逻辑(buildRegistryDeclarations)会把registryOptionsByUrl中的serverType与supportsTimeField逐一写入对应 URL 的声明对象(index.ts):
for (const [registry, options] of Object.entries(context.registryOptionsByUrl ?? {})) { if (options.serverType != null) declarationFor(registry).serverType = options.serverType if (options.supportsTimeField != null) declarationFor(registry).supportsTimeField = options.supportsTimeField }默认注册表(@scope)不参与该声明结构,它作为请求自身的registry字段单独传输;用户未改道的内置路由(如@jsr→npm.jsr.io)也不会被声明,避免每次请求都把一个本不解析 JSR 包的路由塞进 pnpr 服务器的白名单。
Rust 侧的对应实现
作为 pnpm 的 Rust 移植方向,同名能力在pnpm/crates下同样落地:
- settings.rs 与 registry_options.rs:解析
registrySupportsTimeField设置与逐注册表声明; - lockfile/resolution/registry.rs:注释明确指出该结构中的时间字段支持标志"是对
registrySupportsTimeField设置的答案"; - resolving-npm-resolver/src/lookup_context.rs:解析验证器上下文按需携带逐注册表时间支持信息;
- package-manager/src/resolution_policy.rs:解析策略中消费该标志。
行为矩阵速查
| 场景 | 时间基解析下该注册表拉取 |
|---|---|
声明supportsTimeField: true | 简略元数据 |
未声明,全局registrySupportsTimeField: true | 简略元数据 |
| 未声明,全局未设置(默认) | 完整元数据 |
声明supportsTimeField: false(覆盖全局true) | 完整元数据 |
trustPolicy: no-downgrade/supportedArchitectures.libc任意 | 完整元数据(声明不豁免) |
小结
registries.<url>.supportsTimeField让 pnpm 的时间基解析从"全局一刀切"进化为"按注册表精准决策":Verdaccio、GitHub Packages 等自带time字段的私有实例可以声明后继续走轻量简略元数据,而registry.npmjs.org等不提供该字段的注册表才回退到完整文档。配置只需三行 YAML,源码侧的registrySupportsTimeField→needsFullMetadataForRegistry→needsFullMetadataFor→pickPackage调用链清晰可循,并有完整的单测矩阵(shouldFetchFullMetadata.test.ts、toResolvedRegistryDeclarations.test.ts)覆盖优先级、双向覆盖、尾斜杠规范化与过滤镜像一致性等边界语义。
【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考