pnpm v12 Rust 实现 pacquet 开发指南:版本策略、类型建模与测试工程规范
pnpm v12 Rust 实现 pacquet 开发指南版本策略、类型建模与测试工程规范【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm本篇技术指南以pnpm/CLAUDE.md为核心骨架系统讲解 pnpm 仓库中pacquetpnpm v12 的 Rust 实现的工程约定为什么新功能只进 v12 而 v11 仅做维护、如何把 TypeScript 的品牌字符串类型映射为 Rust newtype、如何组织约 11000 个测试、以及从构建命令到提交信息的完整工作流。读完你可以直接在该仓库的pnpm/子项目内独立开发、测试与提交 Rust 代码并能理解每一类约定背后对应的源码位置与设计动机。项目定位pacquet 即 pnpm v12pnpm 仓库是一个同时容纳三个产品的 monorepo根目录的 AGENTS.md 对三者做了总述TypeScript pnpm v11 CLI—— 位于pnpm11/只做 bug 修复与维护Rust pnpm v12 CLIpacquet—— 位于pnpm/是所有新功能开发的唯一目标Rust pnpr registry server—— 位于pnpr/提供与 pnpm 兼容的 npm 注册表实现。pnpm/CLAUDE.md是 pacquet 专属的智能体开发指南它只补充、从不违背仓库根级约定。根AGENTS.md负责跨 monorepo 的通用规则GitHub PR 工作流、代理撰写内容的签名、Conventional Commits、代码复用哲学、绝不忽视测试失败、PR 冲突解决脚本等pnpm/CLAUDE.md则把这些规则细化到 pacquet 的 Rust 代码上并追加 pacquet 独有的条款。版本策略v12 是新功能的家v11 只修 bugCLAUDE.md 用一整节定义了双版本开发政策其要点在根 AGENTS.md 中也有仓库级表述新功能只进 v12。一个新命令、新 flag、新行为或新格式引入 pacquet 后不需要再写一份 TypeScript 实现共有 bug 双版本修复。若同一 bug 同时存在于 v11 与 v12必须在两个实现中分别修复并测试只存在于单侧则只修单侧匹配可观察行为而非代码结构。共享修复要求用户或下游工具观察到的结果一致——CLI flag 与默认值、环境变量处理、lockfile/manifest/state 文件格式、错误码与错误消息、存储布局、钩子语义都在对齐范围内函数拆分的相似性只是交叉引用的便利不是硬性要求版本特有行为要刻意保持。v12 的新特性是有意为之的差异不做 backport实现共享修复时也不要顺手引入无关差异日志输出是共享修复行为的一部分。凡共享修复触发了pnpm:channel事件的函数调用点、payload 与发射次序必须两栈一致这样pnpm/cli.default-reporter解析 pacquet 的 NDJSON 与解析 TypeScript CLI 的方式完全相同协议细节见 pnpm/CODE_STYLE_GUIDE.md优先真实 fixtureDI 缝只在覆盖不到的分支使用。绝大多数正/反路径用tempfile::TempDir、mock 注册表或直接派生真实二进制的集成测试即可依赖注入缝Host提供者上的能力 trait以Sys: Bounds线程化只用于真实 fixture 无法可移植地覆盖的分支文件系统错误类型PermissionDenied、ENOSPC等、确定性时间、测试会污染的全进程共享状态env::set_var、set_current_dir、umask 等以及pnpm login2FA、pnpm publishOIDC/provenance这类外部服务正常路径见 pnpm/CODE_STYLE_GUIDE.md 中的 gating 规则、命名、八原则与modules-yaml实例。动手前如果预期行为不明确或看起来有误停下来询问用户而不是猜测。品牌字符串类型建模从 TypeScript 到 Rust 的八条规则TypeScript 版 pnpm 大量依赖品牌字符串类型branded string一个被幻影属性收窄的普通字符串例如type PkgName string { __brand: PkgName }让类型系统能追踪运行时不可见的意图。有些品牌经由校验构造器打标有些则直接用裸as断言铸造、完全没有运行时校验。CLAUDE.md 强调两个技术栈必须保留这一区别因为它是 pnpm 通过 manifest、lockfile、state 与 config 文件对外暴露的公共契约——TypeScript 品牌与 Rust newtype 必须在校验策略上保持一致。八条建模规则如下声明 newtype 包装器不要退化成普通String/str给类型独立 struct让误用在 pacquet 中同样成为类型错误上游总是先校验再构造 → 你也校验当 pnpm 每个品牌点都经过检查型工厂时pacquet 包装器只能通过TryFromString和/或FromStr构造不得提供接受任意字符串的不可失败公共构造器上游从不校验 → 只为类型安全打品牌某些品牌只用于防止PkgId被误传成PkgName运行时不校验。此时 Rust 侧提供不可失败的FromString方便时再加Fromstr类型安全本身就是全部意义上游偶发不校验构造 → 暴露from_str_unchecked当 pnpm 有时用裸as断言跳过校验器时Rust 侧添加from_str_unchecked或类似命名构造器让调用方显式选择同样的非检查路径校验构造器仍需保留from_str_unchecked是逃生舱而非默认匹配上游 serde 行为品牌类型若跨 JSON/YAML/INI 边界manifest、lockfile、state、config 文件等须接入 serde 让校验策略在序列化后依然生效——反序列化用#[serde(try_from String)]值经校验器进入序列化用#[serde(into String)]往返类型两个都用机械转换用derive_more派生当规则隐含的转换只是包一层/解一层的一行代码时用#[derive(derive_more::From)]/#[derive(derive_more::Into)]而不是手写impl只有需要校验或归一化等自定义逻辑时才手写。derive_more已是 workspace 依赖字符串字面量联合变成enum上游若是auto | always | never这类字面量类型建模为 Rustenum而非 newtype因为合法值集合是封闭的模板字面量类型视为品牌字符串如${string}${string}按规则 2~5 的校验纪律使用 newtype 包装器。源码实例PkgIdWithPatchHashpnpm/crates/lockfile/src/pkg_id_with_patch_hash.rs是规则 3 的直接落地。其文档注释明确写到PerCLAUDE.mds Modeling branded string types section rule 3并给出了完整实现use derive_more::{From, Into}; use serde::{Deserialize, Serialize}; /// The patch-aware package ident used by pnpms side-effects cache and /// dep-graph hashing. A branded string /// (type PkgIdWithPatchHash string { __brand: PkgIdWithPatchHash }). #[derive( Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, From, Into, )] #[serde(transparent)] pub struct PkgIdWithPatchHash(String); impl Fromstr for PkgIdWithPatchHash { fn from(value: str) - Self { PkgIdWithPatchHash(value.to_string()) } }非校验品牌 → 不可失败的FromString/Fromstr经derive_more派生#[serde(transparent)]保证线上格式与String一致该值会进入.modules.yaml或 side-effects-cache 键跨 JSON/YAML 边界。同文件的注释还指明其近亲pnpm_modules_yaml::DepPath是同类规则下的兄弟品牌。再看校验侧pnpm/crates/package-name/src/lib.rs实现了 npmvalidate-npm-package-namev7 的validForOldPackages规则包括空名/点线开头拒绝、node_modules与favicon.ico的 ASCII 大小写不敏感排除、encodeURIComponent往返判定与scope/pkg拆分——这就是规则 2 上游经校验工厂构造 时 Rust 侧校验器的对应物。仓库布局与命令体系一切经根脚本cargo 是最后手段pnpm/内部的布局crates/—— 构成 pacquet 的库与二进制 cratecli、package-manager、package-manifest、lockfile、store-dir、tarball、registry、network、npmrc、fs、executor、diagnostics、testing-utils等tasks/—— 开发者工具integrated-benchmark、micro-benchmark、registry-mockpnpm/CONTRIBUTING.md —— 提交信息格式、写作风格、环境搭建与提交前要跑的自动检查pnpm/CODE_STYLE_GUIDE.md —— 超出cargo fmt、taplo、clippy 之外的手动风格约定。Rust workspace 位于仓库根Cargo.toml、Cargo.lock、rust-toolchain.toml、justfile、.cargo/、.taplo.toml等不在pnpm/内。因此cargo与just都从仓库根执行。统一入口根 package.json 脚本CLAUDE.md 的核心命令约定是构建、检查、lint、测试都走根package.json脚本而不是直接调cargo或just。每个脚本包装了对应的justrecipe 并透传参数。原因很工程化走pnpm启动的任务会被归入pnpm-workspace.yaml的concurrencyGroups当前为cargo: 1、typescript: 1从而把同一台机器上所有 worktree 的构建与测试运行限制在可承载的并发内裸cargo会绕过这个限制。只有单次、无脚本覆盖的操作才降到cargo/taplo级别。主要命令与 package.json 中的脚本一一对应命令底层 recipe作用pnpm ready:rustjust ready跑 CI 同款检查typos、fmt、check、test、lint覆盖 workspace 约 11000 个测试pnpm test:rust-affectedjust test-affected运行工作树改动的 crates 的测试 必要时的 smoke 集是日常改动的默认测试方式pnpm test:rust -p craterun-rust-tests.mjs单个 crate 的测试用just test的净化环境-E filterset进一步收窄pnpm test:rust-smokejust smoke每个 CLI 行为领域各一个端到端测试smokeprofile 见.config/nextest.tomlpnpm test:rustjust testcargo nextest run跑整个 workspacepnpm ci:rust-test/pnpm ci:pnpr-testjust test-pacquet/just test-pnpr单一产品pacquet / pnpr的 cratespnpm lint:rustjust lintcargo clippy --locked --workspace --all-targets -- --deny warningspnpm check:rustjust checkcargo check --locked --workspace --all-targetspnpm build:pnpmcargo build --release --bin pnpm构建发布版 pacquet 二进制just fmtrustfmt.mjs --alltaplo format固定版本的 rustfmt fork TOML 格式化just cli -- args—直接运行 pacquet 二进制just registry-mock args—管理测试用的 mock 注册表just integrated-benchmark args—对比不同修订或与 pnpm 自身对比详见 CONTRIBUTING.md关键纪律警告即错误--deny warnings不要用#[allow(...)]静默它们除非有具体且正当的理由。CI 会在三个平台对每个 PR 跑全量测试因此本地pnpm ready:rust只用于无法点名受影响 crate 集合的改动不是每次交付前的必经步骤。测试规范真实优先、绝不宽容布局与组织测试与被测代码同置标准 Cargo 布局外加每个 crate 的tests/集成测试pacquet 共享 fixture 在crates/testing-utils/src/fixtures/注册表包 fixture 在../pnpr/.fixtures/packages/pnpm-cli的端到端测试是单一 Cargo targetpnpm/crates/cli/tests/suite/下每个文件都是tests/suite/main.rs的一个模块。新增测试文件放入该目录并在main.rs声明直接放在crates/cli/tests/下的文件会变成独立二进制每个都静态链接整个依赖图含pnpr每次改动该 crate 都要多付出约 240 MB 链接成本。cargo nextest无论有多少二进制都会让每个测试跑在独立进程中。文件相对宏include_str!、include_bytes!按包含文件解析因此为tests/写的路径在tests/suite/下要多一层../快照测试使用insta。有意的改动变更快照时仔细审查 diff 后cargo insta review接受绝不盲目接受快照变更需要 mock 注册表的测试通过pnpm-testing-utils自动拉起pnprcargo test/cargo nextest run不需要单独just registry-mock launch步骤。禁止宽容测试测试不得因为构建/运行环境缺工具就静默return早退。像skip_if_no_git()这种先探测再跳过的模式被明确禁止——如果测试需要某个工具直接调用它让既有的.unwrap()/.expect(...)在工具缺失时 panic在环境欠配时让测试失败才是正确信号。宽容会让测试失去意义环境缺工具是环境的问题应该被修好。这条规则尤其针对git、node、npmgit 在开发者机器上无处不在Node.js 是构建 pnpm 的文档化前置条件不存在 pacquet 测试该跑而这三个工具却缺失的现实环境。唯一勉强可接受的例外是平台锁定工具——即使如此也优先#[cfg_attr(target_os windows, ignore ...)]或本 crate 已用于/bin/shshim 的#[cfg(unix)]门而非运行时探测并跳过门对cargo test可见、会出现在测试报告中静默return则不会。窄范围运行与 mtime 陷阱全量套件很慢应锁定正在改的部分# 工作树改动的 crates pnpm test:rust-affected # 单个 crate、单个测试、pnpm-cli suite 的单个模块suite 是单一 target # 用模块过滤器替代按文件的 --test file_stem pnpm test:rust -p pnpm-lockfile pnpm test:rust -E test(name_substring) pnpm test:rust -p pnpm-cli -E test(/^file_stem::/)凡涉及 CLI 的测试都经pnpm test:rust而非裸cargo nextest因为它会像just test一样净化环境的 npm/pnpm 配置。另一个重要细节来自 pnpm/plans/TEST_PORTING.md临时破坏实现来证明测试有效后必须用git restore file回滚绝不能把备份副本移回原位。Cargo 的新鲜度检查基于 mtime保留旧 mtime 的恢复会让由坏源码编译出的产物看起来比源码新后续测试会以无法解释的flaky方式失败——比如无关测试报出不可能状态、单次与全量运行结论不一致。git restore写入全新 mtime 并触发重编译若测试结果在无代码改动时翻转先怀疑陈旧产物touch实现文件重跑。测试移植计划活跃的移植计划在 pnpm/plans/TEST_PORTING.md它枚举了待移植的上游 TypeScript 测试含文件路径与行号及移植约定——known_failures模块、在未实现边界用pnpm_testing_utils::allow_known_failure!包裹、以及临时破坏被测对象以验证移植测试确实能捕获回归的做法。添加移植测试前先查阅它并随落地更新复选框。共享 bug 修复两栈都要移植对应测试给 pacquet 一个覆盖 TypeScript 同场景的 Rust 测试是对共享 bug 被一致修复最直接的证明。代码风格与注释风格指南是唯一事实源pnpm/CODE_STYLE_GUIDE.md 是风格层面的唯一事实源CLAUDE.md 只摘录高亮参数选型优先 minimize copies能拓宽就用最包容的类型Path优于PathBufstr优于String不因额外拷贝而收窄引用计数克隆Arc::clone(x)/Rc::clone(x)优于x.clone()让 O(1) 的 refcount 自增在调用点可见由clippy::clone_on_ref_ptr强制测试日志断言不是assert_eq!时几乎总要日志assert_eq!比较简单标量时几乎不需要日志多行字符串用eprintln!{}复杂结构用dbg!命名遵循 Rust API Guidelines禁止模块体内的星号导入写use super::{Foo, bar}而非use super::*;其他受控模块的 glob 同理。仅两种形式放行外部 crate 的 prelude如use rayon::prelude::*;与模块根的再导出lib.rs里的pub use submodule::*;。注释纪律与根 AGENTS.md 同一基线代码要能自我解释注释服务于不明显的为什么不是对是什么的翻译。Rust 侧追加三条doc 注释///、//!是 rustdoc 可见的 API 文档用于条目契约实现层面的理由放普通//注释测试即文档不要用散文重复。行为场景、边界情况、失败模式若已被测试名字、setup、断言捕获就不要在实现上的 doc 注释里再叙述一遍doc 注释陈述契约一次测试演示行为反之测试的 doc 注释也不该复述断言内容// SAFETY:、// TODO:等前缀是例外用于标记读者仅凭代码无法恢复的隐藏不变量或已知后续工作。优先重命名、重构或提取辅助函数而不是留注释只有当名字与类型确实承载不了信息时才动用散文。保留既有方法链编辑既有代码时不要为风格而把方法链含pipe-trait的.pipe(...)链拆成中间let绑定。可接受的理由只有改动后链无法编译、借用检查拒绝、拆分有实际性能收益、或其他链必须拆的具体原因。纯风格重构在任务无关时不是理由。示例把PathBuf::from分配换成Path::new借用应当留在链内output .stdout .pipe(String::from_utf8) .expect(convert stdout to UTF-8) .trim_end() .pipe(Path::new) // 而不是 PathBuf::from .parent() .expect(parent of root manifest) .to_path_buf()确需拆链时在回复、提交信息或 PR 描述中说明理由让评审者确认重写是必要的纯风格改动应与无关编辑分离单独提出。代码复用与依赖层级根 AGENTS.md 的先搜索再动手/抽取共享代码/偏好成熟 crate/依赖保持在正确层级规则同样适用于 pacquet另有三个 pacquet 专属要点共享辅助代码通常分布在crates/fs、crates/testing-utils、crates/diagnostics先查这三处新增依赖前先查根Cargo.toml的[workspace.dependencies]是否已有合适项依赖保持在正确层级新依赖加给真正需要它的具体 crate而不是 workspace 根或共享 crate——除非多个 crate 确实依赖它。已声明于[workspace.dependencies]的依赖可以加给任何需要的 crate但未声明的新第三方依赖不得擅自添加除非有明确的人工请求。若有明显收益与理由应请人工批准并由其加入[workspace.dependencies]评估候选时参考deny.toml。错误与诊断miette 错误码即公共契约用户可见的错误经pnpm-diagnosticscrate 走miette。pnpm 定义了错误码与错误消息的地方要与其一致——错误码是公共契约的一部分不是实现细节权威清单见 https://pnpm.io/errors。这意味着改动错误路径时v12 与 v11 的代码、消息与退出码必须对齐。提交与 PR 卫生提交保持专注bug 修复提交不应夹带无关的重构或格式化共享修复同时落地同时适用于 v11 的修复要两个实现一起提交必须拆分时交叉引用对应 PR让评审者能确认两侧都已修复推送前自检跑typos、格式化器、pnpm check:rust、pnpm lint:rust及所动 crates 的测试。check与lint保持 workspace 级测试才做范围化只有改动越过可点名的 crate 集合时才动用全量pnpm ready:rust——CI 反正会在三个平台跑全量套件pre-push 钩子仓库级 huskypre-push钩子运行pnpm run pre-push:rust即pnpm/scripts/pre-push-rust.sh检查rustfmt、taplo、cargo clippy --all-targets -D warnings、cargo docRUSTDOCFLAGS-D warnings与cargo dylint。推送前确保环境能跑 cargocargo-dylint运行时探测未安装则告警跳过。提交信息遵循 Conventional Commits完整类型列表见根 AGENTS.mdscope 使用 crate 名或所动领域与既有历史一致git log --oneline取例。pacquet 在标准列表外追加一种类型bench仅涉及基准的改动。来自仓库历史的示例fix(network): set explicit timeouts on default reqwest client feat(lockfile): support npm-alias dependencies in snapshots perf(store-dir): share one read-only StoreIndex across cache lookups红线清单不该做的事最后是 CLAUDE.md 的Things not to do清单相当于对贡献者的行为底线不要在 pnpm v11 中实现新功能、新 flag 或新行为——新功能只属于 v12同一 bug 在 v11 与 v12 中都存在时不要只修一个实现已在根Cargo.toml[workspace.dependencies]声明的依赖可加给任何需要的 crate没有明确人工请求时不要添加 workspace 未声明的依赖有明显收益与理由时请人工批准并加入 workspace 依赖评估时参考deny.toml没有明确理由与评审时不要引入unsafe不要为了 PR 变绿而禁用 lint、测试或 CI 检查。这些红线与根AGENTS.md的永不忽视测试失败共同构成仓库的底线原则测试失败必须被调查并修复若在改动前就坏了也要作为工作的一部分修好而不是静默跳过。深入阅读pnpm/CLAUDE.md —— 本文的直接来源pacquet 专属开发指南AGENTS.md —— 仓库级智能体指南v12/v11 政策、PR 工作流、changeset 规范、提交信息pnpm/CONTRIBUTING.md —— 提交信息格式、环境搭建just init/just install、自动检查、调试TRACEpnpm_tarball just cli add fastify、基准just integrated-benchmarkpnpm/CODE_STYLE_GUIDE.md —— 风格指南全文函数长度、嵌套深度、DI 缝八原则、Reporter/log events 协议、modules-yaml实例pnpm/plans/TEST_PORTING.md —— 测试移植计划known_failures约定、git restore回滚纪律pnpm/crates/lockfile/src/pkg_id_with_patch_hash.rs —— 品牌字符串规则 3 的源码实例pnpm/crates/package-name/src/lib.rs —— npm 包名校验的 Rust 实现pnpm/crates/cli/tests/suite/main.rs —— pnpm-cli 单一 target 端到端套件【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考