RFC:utoo-pm 支持 ut outdated
- 状态:Draft
- 日期:2026-07-13
- 负责人:utoo-pm
- 范围:
crates/pm、crates/ruborist
- 相关 Issue:#3210
摘要
本文提议为 utoo-pm 增加只读命令 ut outdated,用于展示项目依赖中存在新版本的 package。命令语义和输出优先对齐 npm outdated,包含 Current、Wanted 和 Latest 三个版本列;它们不是命令参数:
Current:当前项目实际安装或锁定的版本;
Wanted:按照当前依赖声明重新解析时选择的版本;
Latest:registry latest dist-tag 指向的版本。
本期只检查 root 与目标 workspace 的直接依赖,不检查传递依赖。命令不修改 package.json、package-lock.json 或 node_modules。
背景
utoo 目前支持安装、更新、查看 registry package 信息和输出依赖树,但缺少一个低成本回答以下问题的命令:
- 当前实际安装了哪个版本?
- 在现有 semver range 内最多能升级到哪个版本?
- registry 当前发布的 latest 是哪个版本?
- 哪些依赖需要修改
package.json range 才能升级到 latest?
现有 ut update 会删除 lockfile 并重新安装依赖,不适合作为只读检查手段。用户在执行更新前需要先了解潜在变化。
utoo 已具备实现该能力的主要基础设施:
PackageLock / LockPackage 提供实际解析版本和 importer 的依赖声明;
FullManifest / VersionsInfo 提供 registry versions 和 dist-tags;
resolve_target_version 实现 npm 风格的 dist-tag、latest 优先和 max-satisfying;
- manifest service、磁盘缓存、registry 配置和鉴权可以复用。
因此,本 RFC 不引入新的依赖解析算法,只组合现有 lockfile、registry manifest 和 semver 能力。
目标
- 增加
ut outdated 命令。
- 默认检查当前项目或当前 workspace 的直接依赖。
- 输出 npm 风格的
Package、Current、Wanted、Latest、Location 和 Depended by。
- 覆盖
dependencies、devDependencies、peerDependencies 和 optionalDependencies。
- 支持使用与
ut clean 相同的 package pattern 过滤。
- 支持 monorepo workspace 选择和全 workspace 检查。
- 复用现有 registry、认证、缓存、并发限制和 semver 语义。
- 保持命令完全只读。
非目标
- 不递归报告传递依赖,本期不提供
--all。
- 不修改依赖 range。
- 不修改或重新生成 lockfile。
- 不安装 package,也不修改
node_modules。
- 不取代安全漏洞审计;
outdated 与 audit 是不同能力。
- 首版不为 git、file、link、远程 tarball 等非 registry 来源查询版本;
workspace: 和 catalog: 按本文协议规则处理。
- 首版不提供自动交互式升级。
- 首版不实现 npm 的
--all 和 global mode。
用户体验
默认命令
输出示例:
Package Current Wanted Latest Location Depended by
react 18.2.0 18.3.1 19.1.0 node_modules/react app
typescript 5.7.2 5.7.3 5.9.3 node_modules/typescript app
Package pattern 过滤
位置参数支持一个或多个 package pattern,用于筛选目标 root/workspace 的直接依赖:
ut outdated react
ut outdated 'eslint*'
ut outdated '@types/*'
ut outdated react 'eslint*' '@types/*'
匹配规则直接复用 crate::util::cache::matches_pattern,与 ut clean 保持一致:
*:匹配全部 package;
react:精确匹配;
eslint*:前缀匹配;
*-plugin:后缀匹配;
eslint*plugin:首尾片段匹配;
@types/*:匹配 scope 下的 package。
多个 pattern 采用 OR 语义,命中任意一个即保留;没有位置参数时等价于 *。pattern 只匹配 dependency 的声明名称;npm alias legacy-react: npm:react@^17 应按 legacy-react 匹配,不按真实 registry package 名 react 匹配。
matches_pattern 当前不支持 !pattern 排除,因此本期也不增加排除语法。未来如果 cache-clean 的共享 matcher 增加排除能力,outdated 可自动沿用同一语义。
Workspace
建议提供:
ut outdated --workspace packages/app
ut outdated --workspace '@scope/app'
ut outdated --workspaces
- 不传 workspace 参数:检查当前目录所属 workspace;若当前目录不属于 member,则检查 root package。
--workspace:可重复,复用现有 workspace 名称/路径/glob 选择语义。
--workspaces:检查 root 和所有 workspace member。
Depended by 始终显示声明该依赖的 root package 或 workspace。多 workspace 下同一个 package 可以出现多行,因为不同 importer 的声明范围和实际解析版本可能不同:
Package Current Wanted Latest Location Depended by
react 18.2.0 18.3.1 19.1.0 node_modules/react @utoo/app
版本语义
对于 importer 中声明的 registry 依赖 name: spec,定义:
Current = 实际依赖树中该 importer 依赖边指向的版本;不存在时为 MISSING
Wanted = registry 中按照当前 spec 和安装策略解析出的版本
Latest = registry dist-tags.latest
Current
npm 使用 Arborist.loadActual() 扫描实际安装树,因此 Current 表示磁盘现状,即使 node_modules 与 lockfile 不一致也能报告。Utoo 当前没有等价的完整 actual-tree reader,建议采用两阶段策略:
- 首版以
package-lock.json 中 importer edge 的解析结果作为 Current 和 Location;
- 后续增加 actual-tree reader 后,优先使用磁盘实际版本,lockfile 只辅助建立依赖边和 location;
- 依赖边没有 target 时显示
MISSING,而不是把声明范围当作当前版本。
无论使用哪种来源,实现都必须通过 importer edge 定位 target,不能假设所有直接依赖都位于根 node_modules/{name}。workspace、hoisting 和嵌套安装都可能让同一个 package 名对应多个 location/version。
首版没有 lockfile 时返回可操作错误,建议用户先执行 ut install。这是 Utoo MVP 与 npm actual-tree 行为的明确差异。
Wanted
Wanted 使用当前依赖声明和 registry packument 进行版本选择:
- spec 是 dist-tag 时,选择对应 dist-tag;
- spec 是 semver range 时,复用
resolve_target_version;
- npm alias 先解析真实 package 名和 alias 内部 spec;
- 版本选择应复用 install 的 engines、OS/CPU、prerelease、deprecated 和其他 manifest policy,避免
outdated 推荐一个 ut install 不会选择的版本;
- range 不存在匹配版本时按 npm 的
ETARGET 类错误策略跳过该依赖;
- 非 registry spec 按“协议语义”章节处理。
示例:
声明:^18.0.0
Current:18.2.0
可用版本:18.3.1、19.1.0
Wanted:18.3.1
Latest:19.1.0
Latest
Latest 严格使用 registry dist-tags.latest,不使用版本列表的数学最大值替代。这样与 npm registry 的发布语义一致,也能正确处理维护者移动 dist-tag 的情况。
如果 manifest 没有 latest dist-tag,该 package 记为无 latest,继续处理其他 package。
是否展示
建议首版采用语义明确的判断:
展示条件 = Current 不存在 || Current != Wanted || Wanted != Latest
这比单纯判断 Current < Latest 更稳健,可以展示以下异常或特殊状态:
- 当前版本高于被回退的
latest;
- dist-tag 指向较旧版本;
- 当前 lockfile 版本已不满足 package.json range;
- prerelease 与稳定 dist-tag 产生非线性关系。
这里刻意使用版本 identity 判断,不假设三列单调递增。Wanted 可以高于 Latest,Current 也可以高于 Latest。需要判断 major/minor/patch 时才使用 semver 比较,不能用字符串比较。
npm 10.9.3 参考实现
npm 的完整命令流程如下:
- 使用
Arborist.loadActual() 建立实际安装树;
- workspace filter 通过 Arborist 的 dependency set 限定 edge 来源;
- 无位置参数时收集 root 和 workspace 的直接
edgesOut;
- npm 的
--all 会遍历 inventory 中所有 node 的 edgesOut,因此包含传递依赖;本期 Utoo 不实现该模式;
- 指定 package 名时,查询 inventory 中所有同名 node,再收集其
edgesIn;
- 每条 edge 解析依赖类型、alias、当前 node、location 和 dependent;
- 跳过被
--omit 排除的 node、缺失的非生产依赖,以及非 registry spec;
- 通过
pacote.packument(..., { preferOnline: true }) 获取 packument;
- 通过
npm-pick-manifest 分别计算 Wanted 和 Latest;
- 并发处理所有 edge,按 package 名和 dependent 排序;
- 有任何结果时将退出码设为
1;无结果时不输出内容;
- npm 命令自身支持 pretty、
--json、--parseable 和 --long 四种输出;Utoo 本 RFC 不把这些横切能力建模为 Outdated 专属参数。
npm 对异常的处理不是“所有单包失败都降级”:目标版本不存在或 package 不存在会跳过,其他 registry/网络错误会使命令失败。Utoo 应沿用这一分类原则,但修正 npm 源码注释与实际错误码列表不一致的问题,显式列出可跳过错误。
默认、指定包与 npm --all
ut outdated
root/workspace 直接 edgesOut
npm outdated foo
npm 会查询实际树中名为 foo 的所有 node,再检查它们的 edgesIn
ut outdated 'foo*'
Utoo 使用与 cache-clean 相同的 pattern matcher,
只筛选 root/workspace 中声明名匹配 foo* 的直接 edge
npm outdated --all
npm 扩展模式:实际树所有 node 的 edgesOut,包括传递依赖
本期 Utoo 不实现
本 RFC 采用默认直接依赖查询,并允许通过 package pattern 缩小这些直接 edge 的范围。npm 的指定包查询可能命中传递实例,Utoo 本期不复制这一隐式扩展;传递依赖的 Wanted 受父 package range、override、peer context 和安装布局共同影响,输出噪声较大,本期不引入 --all。
协议语义
outdated 必须先识别原始声明协议,再决定是否访问 registry。不能把所有 spec 都直接交给 semver resolver。
| 声明 |
Current |
Wanted |
Latest |
Registry 请求 |
行为 |
^1.2.0、~1.2.0、exact、tag |
实际 target |
按声明解析 |
latest |
是 |
正常检查 |
npm:real-name@^1 |
alias target |
按真实包 real-name@^1 解析 |
真实包 latest |
是 |
Package 列保留 alias 关系 |
catalog: / catalog:name |
实际 target |
先展开 catalog entry,再按展开后的 spec 解析 |
package latest |
是 |
保留 catalog 来源信息 |
workspace:* 等 |
本地 workspace version |
本地 workspace version |
- |
否 |
默认不作为 registry outdated 项 |
file: / link: / portal: |
本地 target version 或 linked |
linked |
linked |
否 |
默认不输出 |
| git / GitHub |
当前 checkout identity |
不在首版计算 |
- |
否 |
跳过 |
| 远程 tarball |
当前 package version |
不在首版计算 |
- |
否 |
跳过 |
catalog:
Utoo 已支持默认 catalog 和命名 catalog:
{
"dependencies": {
"react": "catalog:",
"lodash": "catalog:legacy"
}
}
catalog: 本身不是 registry range。计算前必须通过项目根配置展开:
react: catalog:
→ default catalog 中 react 的 ^18.0.0
→ Wanted = 按 ^18.0.0 解析的版本
lodash: catalog:legacy
→ legacy catalog 中 lodash 的 ^4.17.0
→ Wanted = 按 ^4.17.0 解析的版本
规则:
- 复用
utoo_ruborist::spec::resolve_catalog_spec,不要重复实现 catalog 查找;
- catalog 或 package entry 不存在时报告配置错误,不能静默把
catalog: 当作 dist-tag;
Current 仍来自每个 importer 的实际 edge target;
Wanted 使用展开后的 range;
Latest 使用 package registry 的 latest;
- 多 workspace 引用同一 catalog dependency 时,首版仍按 dependent edge 分行展示,避免聚合后丢失 importer 信息;
- 内部结果保留
Declared 和 Resolved Spec,例如 catalog:legacy → ^4.17.0,供日志、诊断或未来通用详细输出能力使用。
catalog 配置发生变化但 lockfile 尚未同步时,Current 与新 Wanted 的差异正是 outdated 应展示的信息,不应直接以“lockfile outdated”为由拒绝整个命令。
workspace:
workspace: 表示必须解析到当前 monorepo 的本地 member,不表示去 registry 查询同名 package:
{
"dependencies": {
"@scope/core": "workspace:^"
}
}
规则:
- 通过 workspace discovery 和已 settle 的 workspace edge 找到本地 target;
Current 是本地 workspace package 的 version;
Wanted 也是本地 workspace version,因为 install 目标仍是该 member;
Latest 显示 -,默认不请求 registry,也不因 registry 存在更高版本而报告 outdated;
- member 不存在,或显式 range 与 member version 不兼容时,报告 workspace 配置错误;
workspace:*、workspace:^、workspace:~、workspace:<range> 和 workspace:./path 沿用现有 resolver/pack 语义;
- workspace 协议始终保持本地优先语义。
如果未来需要比较“本地 workspace version 与 registry latest”,应增加独立显式参数,例如 --workspace-registry,避免默认命令产生网络请求并暗示 workspace 应被远程版本替换。
npm alias
例如:
{
"dependencies": {
"legacy-react": "npm:react@^17"
}
}
Registry 请求和版本计算针对 react@^17,但输出 Package 保留声明身份:
legacy-react -> react 17.0.1 17.0.2 19.1.0 ...
manifest cache 按真实 registry package 名 react 去重,不能按 alias 名请求不存在的 package。
依赖类型
内部结果保留 dependencies、devDependencies、peerDependencies 和 optionalDependencies 类型,用于 omit、排序和未来通用详细输出。普通表格不增加 Package Type 列,也不把类型拼接进 package 名。
如果同名依赖出现在多个 section,应遵循 npm package 语义去重。特别是 optionalDependencies 中的声明会覆盖同名 dependencies,不能输出两行冲突结果。
对缺失依赖,首版对齐 npm:缺失的生产依赖显示 MISSING;缺失的 dev、peer、optional dependency 不输出。--omit=dev|peer|optional 应在收集 edge 时过滤对应类型。
为什么不递归传递依赖
ut outdated 的目标是展示用户可以通过当前 importer 的 package.json 直接控制的升级项。传递依赖通常由其父 package 的 range 和 resolver 决定:
app
└── foo ← app 的直接依赖,outdated 检查对象
└── bar ← foo 的传递依赖,不单独报告
递归报告会带来以下问题:
- 用户无法通过修改 app 的
package.json 直接升级 bar;
- 同一个传递 package 可能以多个版本和 peer context 出现;
- 输出规模接近完整 lockfile,噪声远大于可操作信息;
- “可升级版本”必须同时考虑父依赖 range、override 和重新解析布局,语义不再是简单 outdated 查询。
如果未来确实需要完整依赖树检查,应单独提出 RFC,重新定义传递依赖的 edge 选择、override/peer 语义、重复实例展示和性能边界,而不是在本期命令中顺带加入 --all。
Registry 和缓存
实现应先收集所有目标 importer 的直接 registry package 名称,再按名称去重并并发获取 manifest:
读取 importers
→ 收集直接依赖
→ package 名去重
→ 并发获取 manifests
→ 为每条 importer dependency 计算版本
→ 排序和输出
要求:
- 使用现有 registry 配置和 scoped registry 规则;
- 使用现有 token leak guard 和认证逻辑;
- 复用 manifest 内存/磁盘缓存和 ETag;
- 遵守
manifests_concurrency_limit;
- 同一个 package 被多个 workspace 使用时只获取一次 manifest;
ETARGET / E404 类“该依赖不可比较”错误可跳过;鉴权、网络、数据损坏等错误应使命令失败;
- 只请求版本选择所需 metadata,不为本期未展示的 Homepage 等字段获取完整 metadata。
outdated 对新鲜度的需求高于 install 的离线复现需求。默认应允许基于 ETag 向 registry 重新验证缓存,而不是永久使用磁盘中的旧 versions 数据。是否增加 --offline / --prefer-offline 不属于首版范围。
错误处理和退出码
对齐 npm 的退出码:
0:命令成功且没有 outdated package;
1:发现至少一个 outdated/MISSING package,或命令发生致命错误;
- 无结果时不打印空表格和成功文案,保持 stdout 为空;
ETARGET / E404 等可跳过错误不单独改变退出码;
- 鉴权、网络或 packument 解析失败等非可跳过错误终止命令。
退出码 1 同时表示“发现结果”和“执行失败”是 npm 的既有行为。未来可另行评估更细的退出码,但首版优先兼容 npm。
输出排序和颜色
稳定排序规则对齐 npm:先按 package 名,再按 Depended by 排序。
颜色语义对齐 npm:
Current != Wanted:package 名红色,表示当前声明范围内就有可更新版本;
Current == Wanted 且 Wanted != Latest:package 名黄色,表示只有范围外的新版本;
Wanted 青色;
Latest 蓝色。
无 TTY、NO_COLOR 或现有 color 配置禁用颜色时,表格内容必须保持一致。
通用输出能力边界
--json、--parseable 和 --long 属于 CLI 查询命令的通用输出策略,不属于 outdated 版本计算的业务语义。本 RFC 只要求 service 返回稳定、无损的 OutdatedInfo 列表,并由现有或未来的统一 formatter 渲染普通表格。
本期约束:
Commands::Outdated 不声明 json、parseable、long 字段;
service::outdated 不读取 CLI 输出选项;
- 普通模式只输出 npm 风格的核心表格;
- 如果仓库后续增加全局查询输出框架,可直接消费
OutdatedInfo,无需修改版本计算;
- 通用 JSON schema、parseable 分隔格式和 long 字段集合不在本 RFC 中冻结。
实现方案
CLI 层
在 crates/pm/src/cli.rs 增加:
Outdated {
patterns: Vec<String>,
#[arg(short, long)]
workspace: Vec<String>,
#[arg(long)]
workspaces: bool,
}
在 constants.rs 增加命令名和说明,在 main.rs 中路由到 cmd::outdated。
cmd 层
新增 crates/pm/src/cmd/outdated.rs,只负责:
- 定位 project root;
- 将 CLI 参数转换为 workspace/package pattern filter;
- 调用 service;
- 将结果交给 formatter。
业务逻辑不放在 cmd 层。
service 层
新增 crates/pm/src/service/outdated.rs,建议核心模型:
pub struct OutdatedInfo {
pub package: String,
pub registry_package: String,
pub protocol: Protocol,
pub dependency_type: DependencyType,
pub dependent: String,
pub declared: String,
pub resolved_spec: Option<String>,
pub current: Option<String>,
pub wanted: Option<String>,
pub latest: Option<String>,
pub location: Option<String>,
}
核心步骤:
- 加载 root package、workspace 和 package-lock;
- 构建 importer edge 和 target/location 视图;
- 收集 root/workspace 直接 edge;位置参数通过共享
matches_pattern 从这些直接 edge 中筛选;
- 识别协议并展开 alias/catalog,workspace edge 留在本地;
- 按真实 registry package 名去重获取 manifests;
- 通过安装一致的 manifest picker 计算
Wanted;
- 从 dist-tags 读取
Latest;
- 按 npm 展示条件过滤并稳定排序;
- 将稳定结果交给普通表格 formatter 输出。
ruborist 层
首选复用 façade 已暴露的能力。如果 importer dependency 到 lock package target 的查找逻辑目前只存在于 resolver 内部,应在 utoo-ruborist 增加最小、只读的 façade API,而不是从 utoo_ruborist::resolver 或 ::model 内部路径导入。
不应为了 outdated 新建独立 crate。
测试计划
单元测试
^1.0.0:Current 落后于 range 内 Wanted。
- major latest:Wanted 保持当前 major,Latest 进入下一 major。
- exact version:Wanted 等于声明版本,Latest 更高。
- dist-tag spec:Wanted 使用该 dist-tag。
- prerelease range。
- registry latest 回退到低版本。
- Current 不满足当前 range。
- 缺少 latest dist-tag。
- optional 覆盖 dependencies 同名声明。
- 精确、前缀、后缀、中间和 scoped package pattern。
- 多 pattern 使用 OR 语义,空 pattern 列表等价于
*。
- npm alias 按声明名匹配 pattern,使用真实 package 查询并保留 alias 展示名。
catalog: 和命名 catalog 展开后计算 Wanted。
- 缺失 catalog entry 报配置错误。
workspace: 不请求 registry,member 缺失/版本不匹配时报错。
Current > Latest 和 Wanted > Latest 仍正常展示。
- ETARGET/E404 跳过,鉴权/网络错误终止。
集成测试
使用 mock registry,避免测试依赖真实公网:
- 单 package 项目的表格输出。
- 无 outdated package。
- 缺少 lockfile。
- package.json 与 lockfile 不同步时仍能基于 edge 展示 Current/Wanted 差异。
- scoped package 和 scoped registry。
- 多 workspace 使用同一个 package,只请求一次 manifest。
--workspace 选择单个 workspace。
--workspaces 输出正确的 Depended by。
workspace: 本地处理,git、file、link 依赖被正确跳过。
- catalog 被多个 workspace 复用时 manifest 去重且 dependent 信息完整。
- 多 package pattern 正确筛选所有目标 importer 的直接依赖。
- 无颜色输出稳定。
- 有结果退出
1,无结果 stdout 为空且退出 0。
渐进落地
Phase 1:单项目 MVP
- root package 的直接 registry dependencies;
Current / Wanted / Latest;
- 与 cache-clean 一致的 package pattern 过滤;
- mock registry 测试;
- 只读和稳定输出。
Phase 2:完整直接依赖类型与过滤
- dev、peer、optional 和 omit;
- npm alias、
catalog:、workspace:;
- pattern 与 alias、scoped package 的组合行为;
- npm 兼容普通表格、颜色和退出码。
Phase 3:Workspace
- 当前 workspace 自动识别;
--workspace 和 --workspaces;
- manifest 去重和 Depended by;
- workspace importer 到 lock target 的完整测试。
开放问题
- 何时引入 actual-tree reader,使
Current 从 lockfile 语义升级为 npm 的磁盘实际语义?
- 缺少 lockfile 时应失败,还是扫描
node_modules 并继续?MVP 建议失败。
--workspaces 是否包含 root package?本 RFC 建议包含。
- 可跳过的 Registry 错误是否只限 ETARGET/E404,还是还包括 E403?应在实现前与 npm 当前行为和 Utoo registry error 类型一起确认。
catalog: 多 workspace 行是否在普通表格聚合?建议首版不聚合,保持每行对应一个 dependent edge。
workspace: 是否需要显式 --workspace-registry 比较远端 latest?默认不比较。
决策摘要
本 RFC 建议接受以下首要设计决策:
ut outdated 是只读命令;
- 只检查 root/workspace 直接依赖,不提供
--all;
Current 的目标语义是实际安装版本,MVP 先从 lockfile edge 获取;
Wanted 按当前声明和 install 一致的 manifest policy 解析;
Latest 来自 registry latest dist-tag,不保证高于 Wanted/Current;
catalog: 展开后查询 registry,workspace: 保持本地且默认不查询 registry;
- npm alias 按真实 package 查询,保留声明 alias;
- registry manifest 按真实 package 名去重并发获取;
- 普通表格、退出码和过滤行为优先对齐 npm;通用输出格式另行设计;
- 首版优先交付单项目 MVP,完整协议和 workspace 支持分阶段落地。
RFC:utoo-pm 支持
ut outdatedcrates/pm、crates/ruborist摘要
本文提议为
utoo-pm增加只读命令ut outdated,用于展示项目依赖中存在新版本的 package。命令语义和输出优先对齐npm outdated,包含Current、Wanted和Latest三个版本列;它们不是命令参数:Current:当前项目实际安装或锁定的版本;Wanted:按照当前依赖声明重新解析时选择的版本;Latest:registrylatestdist-tag 指向的版本。本期只检查 root 与目标 workspace 的直接依赖,不检查传递依赖。命令不修改
package.json、package-lock.json或node_modules。背景
utoo 目前支持安装、更新、查看 registry package 信息和输出依赖树,但缺少一个低成本回答以下问题的命令:
package.jsonrange 才能升级到 latest?现有
ut update会删除 lockfile 并重新安装依赖,不适合作为只读检查手段。用户在执行更新前需要先了解潜在变化。utoo 已具备实现该能力的主要基础设施:
PackageLock/LockPackage提供实际解析版本和 importer 的依赖声明;FullManifest/VersionsInfo提供 registry versions 和 dist-tags;resolve_target_version实现 npm 风格的 dist-tag、latest优先和 max-satisfying;因此,本 RFC 不引入新的依赖解析算法,只组合现有 lockfile、registry manifest 和 semver 能力。
目标
ut outdated命令。Package、Current、Wanted、Latest、Location和Depended by。dependencies、devDependencies、peerDependencies和optionalDependencies。ut clean相同的 package pattern 过滤。非目标
--all。node_modules。outdated与audit是不同能力。workspace:和catalog:按本文协议规则处理。--all和 global mode。用户体验
默认命令
输出示例:
Package pattern 过滤
位置参数支持一个或多个 package pattern,用于筛选目标 root/workspace 的直接依赖:
匹配规则直接复用
crate::util::cache::matches_pattern,与ut clean保持一致:*:匹配全部 package;react:精确匹配;eslint*:前缀匹配;*-plugin:后缀匹配;eslint*plugin:首尾片段匹配;@types/*:匹配 scope 下的 package。多个 pattern 采用 OR 语义,命中任意一个即保留;没有位置参数时等价于
*。pattern 只匹配 dependency 的声明名称;npm aliaslegacy-react: npm:react@^17应按legacy-react匹配,不按真实 registry package 名react匹配。matches_pattern当前不支持!pattern排除,因此本期也不增加排除语法。未来如果 cache-clean 的共享 matcher 增加排除能力,outdated可自动沿用同一语义。Workspace
建议提供:
ut outdated --workspace packages/app ut outdated --workspace '@scope/app' ut outdated --workspaces--workspace:可重复,复用现有 workspace 名称/路径/glob 选择语义。--workspaces:检查 root 和所有 workspace member。Depended by始终显示声明该依赖的 root package 或 workspace。多 workspace 下同一个 package 可以出现多行,因为不同 importer 的声明范围和实际解析版本可能不同:版本语义
对于 importer 中声明的 registry 依赖
name: spec,定义:Current
npm 使用
Arborist.loadActual()扫描实际安装树,因此Current表示磁盘现状,即使node_modules与 lockfile 不一致也能报告。Utoo 当前没有等价的完整 actual-tree reader,建议采用两阶段策略:package-lock.json中 importer edge 的解析结果作为Current和Location;MISSING,而不是把声明范围当作当前版本。无论使用哪种来源,实现都必须通过 importer edge 定位 target,不能假设所有直接依赖都位于根
node_modules/{name}。workspace、hoisting 和嵌套安装都可能让同一个 package 名对应多个 location/version。首版没有 lockfile 时返回可操作错误,建议用户先执行
ut install。这是 Utoo MVP 与 npm actual-tree 行为的明确差异。Wanted
Wanted使用当前依赖声明和 registry packument 进行版本选择:resolve_target_version;outdated推荐一个ut install不会选择的版本;ETARGET类错误策略跳过该依赖;示例:
Latest
Latest严格使用 registrydist-tags.latest,不使用版本列表的数学最大值替代。这样与 npm registry 的发布语义一致,也能正确处理维护者移动 dist-tag 的情况。如果 manifest 没有
latestdist-tag,该 package 记为无 latest,继续处理其他 package。是否展示
建议首版采用语义明确的判断:
这比单纯判断
Current < Latest更稳健,可以展示以下异常或特殊状态:latest;这里刻意使用版本 identity 判断,不假设三列单调递增。
Wanted可以高于Latest,Current也可以高于Latest。需要判断 major/minor/patch 时才使用 semver 比较,不能用字符串比较。npm 10.9.3 参考实现
npm 的完整命令流程如下:
Arborist.loadActual()建立实际安装树;edgesOut;--all会遍历 inventory 中所有 node 的edgesOut,因此包含传递依赖;本期 Utoo 不实现该模式;edgesIn;--omit排除的 node、缺失的非生产依赖,以及非 registry spec;pacote.packument(..., { preferOnline: true })获取 packument;npm-pick-manifest分别计算Wanted和Latest;1;无结果时不输出内容;--json、--parseable和--long四种输出;Utoo 本 RFC 不把这些横切能力建模为Outdated专属参数。npm 对异常的处理不是“所有单包失败都降级”:目标版本不存在或 package 不存在会跳过,其他 registry/网络错误会使命令失败。Utoo 应沿用这一分类原则,但修正 npm 源码注释与实际错误码列表不一致的问题,显式列出可跳过错误。
默认、指定包与 npm
--all本 RFC 采用默认直接依赖查询,并允许通过 package pattern 缩小这些直接 edge 的范围。npm 的指定包查询可能命中传递实例,Utoo 本期不复制这一隐式扩展;传递依赖的
Wanted受父 package range、override、peer context 和安装布局共同影响,输出噪声较大,本期不引入--all。协议语义
outdated必须先识别原始声明协议,再决定是否访问 registry。不能把所有 spec 都直接交给 semver resolver。^1.2.0、~1.2.0、exact、taglatestnpm:real-name@^1real-name@^1解析latestcatalog:/catalog:namelatestworkspace:*等-file:/link:/portal:linkedlinkedlinked--catalog:Utoo 已支持默认 catalog 和命名 catalog:
{ "dependencies": { "react": "catalog:", "lodash": "catalog:legacy" } }catalog:本身不是 registry range。计算前必须通过项目根配置展开:规则:
utoo_ruborist::spec::resolve_catalog_spec,不要重复实现 catalog 查找;catalog:当作 dist-tag;Current仍来自每个 importer 的实际 edge target;Wanted使用展开后的 range;Latest使用 package registry 的latest;Declared和Resolved Spec,例如catalog:legacy → ^4.17.0,供日志、诊断或未来通用详细输出能力使用。catalog 配置发生变化但 lockfile 尚未同步时,
Current与新Wanted的差异正是outdated应展示的信息,不应直接以“lockfile outdated”为由拒绝整个命令。workspace:workspace:表示必须解析到当前 monorepo 的本地 member,不表示去 registry 查询同名 package:{ "dependencies": { "@scope/core": "workspace:^" } }规则:
Current是本地 workspace package 的version;Wanted也是本地 workspace version,因为 install 目标仍是该 member;Latest显示-,默认不请求 registry,也不因 registry 存在更高版本而报告 outdated;workspace:*、workspace:^、workspace:~、workspace:<range>和workspace:./path沿用现有 resolver/pack 语义;如果未来需要比较“本地 workspace version 与 registry latest”,应增加独立显式参数,例如
--workspace-registry,避免默认命令产生网络请求并暗示 workspace 应被远程版本替换。npm alias
例如:
{ "dependencies": { "legacy-react": "npm:react@^17" } }Registry 请求和版本计算针对
react@^17,但输出 Package 保留声明身份:manifest cache 按真实 registry package 名
react去重,不能按 alias 名请求不存在的 package。依赖类型
内部结果保留
dependencies、devDependencies、peerDependencies和optionalDependencies类型,用于 omit、排序和未来通用详细输出。普通表格不增加Package Type列,也不把类型拼接进 package 名。如果同名依赖出现在多个 section,应遵循 npm package 语义去重。特别是
optionalDependencies中的声明会覆盖同名dependencies,不能输出两行冲突结果。对缺失依赖,首版对齐 npm:缺失的生产依赖显示
MISSING;缺失的 dev、peer、optional dependency 不输出。--omit=dev|peer|optional应在收集 edge 时过滤对应类型。为什么不递归传递依赖
ut outdated的目标是展示用户可以通过当前 importer 的package.json直接控制的升级项。传递依赖通常由其父 package 的 range 和 resolver 决定:递归报告会带来以下问题:
package.json直接升级bar;如果未来确实需要完整依赖树检查,应单独提出 RFC,重新定义传递依赖的 edge 选择、override/peer 语义、重复实例展示和性能边界,而不是在本期命令中顺带加入
--all。Registry 和缓存
实现应先收集所有目标 importer 的直接 registry package 名称,再按名称去重并并发获取 manifest:
要求:
manifests_concurrency_limit;ETARGET/E404类“该依赖不可比较”错误可跳过;鉴权、网络、数据损坏等错误应使命令失败;outdated对新鲜度的需求高于 install 的离线复现需求。默认应允许基于 ETag 向 registry 重新验证缓存,而不是永久使用磁盘中的旧 versions 数据。是否增加--offline/--prefer-offline不属于首版范围。错误处理和退出码
对齐 npm 的退出码:
0:命令成功且没有 outdated package;1:发现至少一个 outdated/MISSING package,或命令发生致命错误;ETARGET/E404等可跳过错误不单独改变退出码;退出码
1同时表示“发现结果”和“执行失败”是 npm 的既有行为。未来可另行评估更细的退出码,但首版优先兼容 npm。输出排序和颜色
稳定排序规则对齐 npm:先按 package 名,再按
Depended by排序。颜色语义对齐 npm:
Current != Wanted:package 名红色,表示当前声明范围内就有可更新版本;Current == Wanted且Wanted != Latest:package 名黄色,表示只有范围外的新版本;Wanted青色;Latest蓝色。无 TTY、
NO_COLOR或现有 color 配置禁用颜色时,表格内容必须保持一致。通用输出能力边界
--json、--parseable和--long属于 CLI 查询命令的通用输出策略,不属于 outdated 版本计算的业务语义。本 RFC 只要求 service 返回稳定、无损的OutdatedInfo列表,并由现有或未来的统一 formatter 渲染普通表格。本期约束:
Commands::Outdated不声明json、parseable、long字段;service::outdated不读取 CLI 输出选项;OutdatedInfo,无需修改版本计算;实现方案
CLI 层
在
crates/pm/src/cli.rs增加:在
constants.rs增加命令名和说明,在main.rs中路由到cmd::outdated。cmd 层
新增
crates/pm/src/cmd/outdated.rs,只负责:业务逻辑不放在 cmd 层。
service 层
新增
crates/pm/src/service/outdated.rs,建议核心模型:核心步骤:
matches_pattern从这些直接 edge 中筛选;Wanted;Latest;ruborist 层
首选复用 façade 已暴露的能力。如果 importer dependency 到 lock package target 的查找逻辑目前只存在于 resolver 内部,应在
utoo-ruborist增加最小、只读的 façade API,而不是从utoo_ruborist::resolver或::model内部路径导入。不应为了
outdated新建独立 crate。测试计划
单元测试
^1.0.0:Current 落后于 range 内 Wanted。*。catalog:和命名 catalog 展开后计算 Wanted。workspace:不请求 registry,member 缺失/版本不匹配时报错。Current > Latest和Wanted > Latest仍正常展示。集成测试
使用 mock registry,避免测试依赖真实公网:
--workspace选择单个 workspace。--workspaces输出正确的 Depended by。workspace:本地处理,git、file、link 依赖被正确跳过。1,无结果 stdout 为空且退出0。渐进落地
Phase 1:单项目 MVP
Current/Wanted/Latest;Phase 2:完整直接依赖类型与过滤
catalog:、workspace:;Phase 3:Workspace
--workspace和--workspaces;开放问题
Current从 lockfile 语义升级为 npm 的磁盘实际语义?node_modules并继续?MVP 建议失败。--workspaces是否包含 root package?本 RFC 建议包含。catalog:多 workspace 行是否在普通表格聚合?建议首版不聚合,保持每行对应一个 dependent edge。workspace:是否需要显式--workspace-registry比较远端 latest?默认不比较。决策摘要
本 RFC 建议接受以下首要设计决策:
ut outdated是只读命令;--all;Current的目标语义是实际安装版本,MVP 先从 lockfile edge 获取;Wanted按当前声明和 install 一致的 manifest policy 解析;Latest来自 registrylatestdist-tag,不保证高于 Wanted/Current;catalog:展开后查询 registry,workspace:保持本地且默认不查询 registry;