Inquirer.js 仓库的发布利器:@sboudrias/package 包元数据工具详解

原创2026-09-30 15:36:091,437 阅读
文章标签:CLI开发工具

Inquirer.js 仓库的发布利器:@sboudrias/package 包元数据工具详解

本指南围绕 Inquirer.js 仓库中维护的 tools/package 工具包展开,它是一个面向 JavaScript 包与 Monorepo 的元数据(package.json)管理与发布校验 CLI。你将掌握 package lint、package normalize、package pin <dependency> 三个命令的完整用法、底层实现原理,以及 Inquirer.js 发布流水线(prepack 阶段)如何依赖它们保证发布的 npm 包元数据合法、版本精确、类型安全。

工具概览:@sboudrias/package 是什么

@sboudrias/package 是 Inquirer.js Monorepo 内置的一组“包元数据工具”,源码位于 tools/package/src,用途覆盖两类场景:

  • 开发期校验:让所有公开的 workspace 包的 package.json 保持一致、合法(peer 依赖可见、Node 版本范围正确、exports 完整);
  • 发布期修复:在打包 tarball 前就地改写清单,剔除 workspace:* 协议残留、把关键依赖 pin 成精确版本。

它不负责构建代码,只负责“元数据卫生”。所有命令都通过一个基于 clipanion 暴露,注册了 lint、normalize、pin 以及内建 help 四个子命令。命令实现分别位于 lint/command.ts、normalize/command.ts、pin/command.ts。

安装

作为开发期工具,按 devDependency 安装到你的项目中即可:

包管理器 命令
npm npm install @sboudrias/package --save-dev
yarn yarn add @sboudrias/package --dev
pnpm pnpm add @sboudrias/package --save-dev
bun bun add @sboudrias/package --dev

安装后,仓库根 package.json 的 workspaces 会被自动识别(详见下文“Workspace Discovery”)。需要注意的是,该工具自身对运行环境有要求——tools/package/package.json 中声明 engines.node 为 >=23.5.0 || ^22.13.0 || ^20.17.0,请在兼容的 Node.js 版本下使用。

命令总览

命令 说明
package lint 校验并修复包元数据
package normalize 修复 workspace 清单,使其可被 npm 正常发布
package pin <dependency> 将某个 workspace 依赖 pin 为精确版本

所有命令都只作用于公开(public)的 workspace 包(private: true 的包会被跳过,见 Workspace Discovery),并且就地写回清单文件——不存在“干跑一次生成报告再手动应用”的两段式流程,这是与一般 lint 工具最大的使用差异。

package lint

package lint

package lint 校验公开 workspace 包,并就地修复安全的元数据问题。若加上 --check:

package lint --check

则只做校验、不写任何文件;当存在需要修复的包或“人工冲突”(无法自动修复的 error 级问题)时,进程以非零码退出,便于接入 CI。

从 src/lint/index.ts 的实现看,每个问题会按 error / fixed / would fix 三种状态分类(--check 模式下本可修复的问题标记为 would fix),最终通过 hasFailures 决定退出码:只要存在非 fixed 状态的问题即返回 1。lint 覆盖三类检查项,下面逐一说明。

校验 1:补全 Peer 依赖(Valid Peer Dependencies)

场景:某个包 A 的运行时依赖(dependencies / optionalDependencies)B 自身声明了 peer 依赖。对发布后的使用者而言,B 的 peer 要求必须“浮出水面”——由 A 直接声明,否则安装 A 时 npm 不会为 B 的 peer 生成提示。

package lint 做两件事:

  • 把缺失的 peer 加入 A 的 peerDependencies(版本号沿用 B 声明);
  • 复制 B 中匹配的 peerDependenciesMeta 条目,确保“可选 peer 仍然是可选的”。

底层逻辑在 src/peer-dependencies.ts:getPeerDependencyHoists 遍历包的运行时依赖,若某个 peer 既未被包本身运行时依赖、也未在 peerDependencies 中声明,就产生一次 peer-dependency 类型的 hoist;若包的 peerDependenciesMeta 缺失对应条目,则额外产生 peer-dependency-meta 类型的 hoist。applyPeerDependencyHoists 负责把它们写回清单。对应测试见 src/lint/index.test.ts。

校验 2:匹配 Node 引擎范围(Matching engines)

包只应声明其运行时依赖同样能支持的 Node.js 版本。package lint 会:

  • 将缺失、无效(无法被 semver.validRange 解析)或超出根包范围的 engines.node,统一改写为根包 engines.node 的范围;
  • 若某个运行时依赖支持的 Node 范围比本包更窄,则报 error(不可自动修复)。

这个 error 是有意设计的:升级依赖可能抬高最低 Node 版本,从而让仍按旧 engines.node 范围安装本包的依赖方在升级时被破坏。此时正确做法是手动收窄包的 engine 范围,或改选兼容版本的依赖。源码依据在 src/lint/index.ts(根包必须存在合法 engines.node 范围,否则直接抛错)与 src/lint/index.ts(逐依赖比对 subset(packageRange, dependencyRange))。

校验 3:暴露 package.json(Ensure package.json is exposed)

很多工具(如 bundler、元数据探查脚本)会在运行时读取包的清单。package lint 确保每个公开包在 exports 中暴露 "./package.json": "./package.json"。修复策略取决于现有 exports 形态(见 src/lint/index.ts):

  • exports 缺失 → 直接生成 { "./package.json": "./package.json" };
  • exports 是字符串(如 "./src/index.ts")→ 改写为 { ".": "./src/index.ts", "./package.json": "./package.json" };
  • exports 是对象 → 追加 "./package.json" 键;
  • 其他不支持的形态 → 报 error,要求人工补写。

package normalize

package normalize

package normalize 就地应用所有“让公开 workspace 包可被 npm 正常发布”的清单修复。目前它只做一件事:删除以 workspace:* 协议声明的 devDependencies。原因很直接:workspace:* 只存在于 workspace 内部解析,发布后该 spec 无效,必须在打包前从清单中剥离。其他 spec 的 dev 依赖不受影响。

实现位于 src/normalize/index.ts:fixWorkspaceDevDependencies 过滤出 spec 恰为 workspace:* 的 dev 依赖并移除(跳过 private: true 的包),随后把变更写回对应文件,并返回 ManifestFixup[] 供调用方打印 [fixed] ... 日志。工具自身的 tools/package/package.json 中 "@repo/tsconfig": "workspace:*" 就是这类会被剥离的典型条目。

该命令按“可扩展修复集合”设计(FIXUPS 数组):随着新的 npm 合法性问题被发现,后续 fixup 会追加到这个命令,而不是散落在发布脚本里。Inquirer.js 的发布流水线在 prepack 阶段(打包 tarball 前一刻)运行它。

package pin

package pin @inquirer/type

package pin <dependency> 把指定依赖在所有公开 workspace 包中的 semver 范围(^1.2.3、~1.2.3)就地改写为精确版本(1.2.3)。规则细节(见 src/pin/index.ts):

  • 只处理 dependencies 与 optionalDependencies 两个字段,devDependencies 不动;
  • 已经是精确版本的 spec、workspace:* 协议条目一律跳过;
  • 以 ^ / ~ 开头且剩余部分是合法版本号的范围会被 pin;否则(如 >=1.0.0、1.x 这种无法收敛到单一精确版本的范围)报为 unpinnable,命令以非零码退出,需要人工处理。

命令一次接受一个依赖名(pin/command.ts 中 dependencyName 为必填参数),但底层 API pinDependencies 支持一次传入多个名字(内部会去重)。

为什么 Inquirer.js 要在 prepack 时 pin @inquirer/type

README 与源码共同说明了一个很关键的发布场景:Inquirer.js 的发布流水线在 prepack 阶段用 package pin @inquirer/type 把类型依赖 pin 成精确版本。原因是类型专用依赖会泄漏进下游消费者的 .d.ts 编译——即使运行时行为完全不变,未受控的 semver 范围(例如未来 @inquirer/type 发布不兼容的 minor 版本)也可能破坏下游 TypeScript 构建。这是“运行时宽容、类型严格”的经典取舍:运行时依赖可以用宽松范围,类型依赖必须钉死。

Workspace Discovery(工作区发现)

CLI 通过两种来源发现 workspace:

  • 根 package.json 的 workspaces 字段(数组形式,或 { "packages": [...] } 对象形式);
  • pnpm-workspace.yaml / pnpm-workspace.yml 文件中的 packages 列表。

实现见 src/workspaces.ts:readWorkspaceProject 读取根 package.json,合并两种来源的 glob 模式,把形如 packages/* 的模式归一化为 packages/*/package.json(保留 ! 排除语义),用 globby 解析出所有包,并排除 **/node_modules。

两个关键行为:

  • 无 workspace 配置时,根 package.json 被当作单包项目处理(packages 数组中只有它自己),因此该工具同样适用于非 Monorepo 的普通 npm 包;
  • private: true 的包默认被忽略——它们不参与 lint、normalize 与 pin,避免内部包干扰公开发布流程。

在 Inquirer.js 仓库中的实战定位

tools/package 是 Inquirer.js 发布流水线的一部分,但它的 API 设计完全通用,可独立安装到任何 JS/TS Monorepo 使用:

  • 开发期:package lint --check 接入 CI,防止 peer 依赖缺失、Node 范围漂移、exports 不完整等元数据问题悄悄进入主干;
  • 发布期:package normalize && package pin <type-dependency> && npm pack 作为 prepack 的固定编排,保证 tarball 里的清单干净、类型依赖精确。

如果你想在本地复现效果,可以直接在 Inquirer.js 仓库根目录运行 node tools/package/src/bin.ts lint --check(或通过仓库脚本),观察它对 packages/ 下各公开包的校验输出;对应的单元测试分别位于 src/lint/index.test.ts、src/normalize/index.test.ts 和 src/pin/index.test.ts,是理解每个修复规则边界(包括各种不可修复的 error 场景)的最好教材。

登录后查看全文
Inquirer.js