pnpm 按注册表声明 `time` 字段:让 `resolutionMode: time-based` 的完整元数据回退按需生效
2026/9/19 22:52:18 网站建设 项目流程

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.yamlregistries中为某个注册表声明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.yamlregistries映射下,作为每个注册表 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 }

三层回退一目了然:

  1. 该注册表 URL 在registryOptionsByUrl中的supportsTimeField声明(按规范化后的 URL 匹配);
  2. 全局registrySupportsTimeField设置;
  3. 默认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必须按最苛刻的注册表回答——即假设supportsTimeFieldfalse时的策略结果(见 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中的serverTypesupportsTimeField逐一写入对应 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字段单独传输;用户未改道的内置路由(如@jsrnpm.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,源码侧的registrySupportsTimeFieldneedsFullMetadataForRegistryneedsFullMetadataForpickPackage调用链清晰可循,并有完整的单测矩阵(shouldFetchFullMetadata.test.ts、toResolvedRegistryDeclarations.test.ts)覆盖优先级、双向覆盖、尾斜杠规范化与过滤镜像一致性等边界语义。

【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询