使用 @turbo/repository 的 StaticWorkspace 进行无子进程的多语言 Monorepo 静态分析与受影响包推断
使用 turbo/repository 的 StaticWorkspace 进行无子进程的多语言 Monorepo 静态分析与受影响包推断【免费下载链接】turboBuild system optimized for JavaScript and TypeScript, written in Rust项目地址: https://gitcode.com/gh_mirrors/tu/turbo本篇文章围绕turbo/repository中面向 JavaScript 的StaticWorkspaceAPI 展开讲解如何在不调用 Cargo、rustc、uv、Python、Go 等语言工具链的前提下对包含 JavaScript、RustCargo、Pythonuv与 Go 工作区的多语言 monorepo 进行包清单盘点与保守的受影响包affected candidates推断。读完本文你将掌握StaticWorkspace.find、findPackages、workspaceRoots、affectedCandidates的完整用法理解dependencyGraphComplete、affectednessComplete、unloadedToolchains等状态属性的语义并了解其在 Turborepo 源码中的实现原理与边界行为。背景什么是静态仓库分析Turborepo 是面向 JavaScript 与 TypeScript 的构建系统其核心用 Rust 实现。turbo/repository包把这些仓库分析能力以 N-API 的形式封装给 JavaScript 使用参见 packages/turbo-repository/README.md其中rust/目录是围绕核心 Rust 代码的薄封装js/目录负责加载平台相关的原生.node模块并提供类型定义。本文对应的官方文档位于 packages/turbo-repository/js/README.md。文档明确说明该包从 JavaScript API 提供对JavaScript、Cargo、uv 和 Go 工作区的仓库分析能力原生发现逻辑遵循turbo.json中对应的三个 future flagsexperimentalCargoWorkspaces启用 CargoRust工作区发现experimentalPythonWorkspaces启用 uvPython工作区发现experimentalGoWorkspaces启用 Go 工作区发现。需要注意该包目前尚不是稳定版本API、命名与功能均可能发生变化。“静态”在这里的含义是只解析清单文件manifest与配置文件不运行任何语言工具链可执行文件从而在 CI、依赖审计、变更影响分析等场景中获得快速且可预测的结果。StaticWorkspace无需语言工具的包清单与受影响推断StaticWorkspace是本文的核心类。官方文档给出了最小示例import { StaticWorkspace } from turbo/repository; const workspace await StaticWorkspace.find(); // 或传入目录 const packages await workspace.findPackages(); const roots workspace.workspaceRoots(); const { packages: candidates, conservative } await workspace.affectedCandidates([packages/ui/src/button.ts]); console.log(workspace.absolutePath, workspace.dependencyGraphComplete); console.log( workspace.unloadedToolchains, packages, roots, candidates, conservative );这段代码展示了四个核心能力调用作用StaticWorkspace.find(path?)从给定路径缺省为当前工作目录向上推断仓库根并返回实例workspace.findPackages()返回真实的包清单排除仓库根与聚合执行作用域workspace.workspaceRoots()返回各语言工作区根信息workspace.affectedCandidates(files)根据工作区相对路径的变更文件返回可能受影响的包及其传递依赖从类型定义packages/turbo-repository/js/index.d.ts可以看到StaticWorkspace是刻意与Workspace分离的原生依赖边与任务元数据不会加载也无法通过该对象请求但它与Workspace.find共享同一套 JavaScript 工作区根推断与工具链 feature flag 逻辑。find()复用 JavaScript 工作区根推断官方文档强调find(path?)与Workspace.find使用相同的 JavaScript 工作区根与包管理器推断逻辑包括在包管理器未声明时基于 lockfile 的推断它不会添加独立的纯原生根推断。这一点在 Rust 源码 packages/turbo-repository/rust/src/internal.rs 中得到了印证find_with_mode通过WorkspaceState::infer推断仓库状态当packageManager字段缺失时会回退到PackageManager::detect_package_manager做 lockfile 检测——这与 CLI 的--dangerously-allow-missing-package-manager行为一致因为该库分析的是它并不控制的仓库。测试 packages/turbo-repository/tests/static-discovery.test.ts 中 uses existing JavaScript root and lockfile-based package-manager inference 用例验证了这一点在一个没有声明包管理器的npm-monorepo-no-pm夹具上StaticWorkspace.find()、find(.)与find(root)都能得到相同的结果。此外does not infer a native-only root without the JavaScript workspace context 用例证明在没有package.json与 lockfile 的纯原生仓库根上调用StaticWorkspace.find(root)会直接拒绝。findPackages()包清单的结构与排序findPackages()返回的包是普通对象plain object字段定义如下字段含义name包名absolutePath包根目录的绝对路径relativePath从工作区根到包根目录的相对路径manifestPath清单文件路径相对于工作区根例如package.json、Cargo.toml、pyproject.toml、go.modtoolchain语言生态标识语言toolchainID 为javascript、rust、python、go。清单来自对应的原生清单文件类型定义packages/turbo-repository/js/index.d.ts对此有明确注释。官方文档说明了三条盘点规则共置包co-located packages会被保留同一个目录下同时存在 JavaScript 包与 Rust 包时两者都会出现在清单中仓库根与原生工作区聚合体被排除例如 Cargo workspace 的聚合根、uv 的聚合 pyproject 不会当作真实包排序规则先按relativePath再按toolchain最后按name。这些规则与实现一一对应。在 packages/turbo-repository/rust/src/static_workspace.rs 的inventory()中包通过graph.package_task_contexts()枚举并用graph.is_real_package(...)过滤掉聚合执行作用域最后按(relative_path, toolchain, name)排序。测试 preserves co-located JavaScript and Rust packages in deterministic order 专门验证了共置包的有序输出。workspaceRoots()各语言工作区根workspaceRoots()返回普通的{ toolchain, kind, relativePath }对象其中toolchain是语言 IDjavascript、rust、python、gokind是原生工作区根的类型cargo、uv、goJavaScript 侧则为对应包管理器类型如npm、pnpm等relativePath是该工作区根相对于仓库根的路径。测试夹具polyglot-monorepo见 packages/turbo-repository/tests/fixtures/polyglot-monorepo同时包含Cargo.toml、pyproject.tomluv workspace、go.work与 pnpm workspace测试中得到的 roots 为四个根go/go、javascript/npm、python/uv、rust/cargo均按(toolchain, kind, relativePath)排序对应 static_workspace.rs 中的roots.sort_by。状态属性三个关键的 getterStaticWorkspace提供三个只读状态属性用于判断当前仓库分析是否完整dependencyGraphCompletedependencyGraphComplete在原生权威元数据尚未加载时为 false。从源码看static_workspace.rs它实现为workspace.graph()?.unloaded_owners().is_empty()——即包依赖关系是否完全加载但这不声明任务级输入或执行契约的完整性。affectednessCompleteaffectednessComplete是独立于前者的 getter表示声明的本地包输入是否可以静态推断。实现上它检查静态受影响知识是否存在且is_complete()见 crates/turborepo-repository/src/static_dependencies.rs 的StaticAffectedness::is_complete。官方文档特别警告不要用dependencyGraphComplete为 false 就去禁用选择性的原生受影响推断。在混合仓库中常见的组合是dependencyGraphComplete false而affectednessComplete true——即原生元数据虽未加载但静态受影响推断仍然可用。测试 selectively propagates Cargo and uv dependencies without language tools 就断言了这种组合。unloadedToolchainsunloadedToolchains返回拥有已盘点作用域但其权威元数据缺失的工具链列表例如[rust, python, go]。当experimental*flags 全部开启但原生元数据未加载时dependencyGraphComplete为 false、unloadedToolchains非空而当 flags 关闭时对应工具链既不会被盘点也不会出现在unloadedToolchains中测试 suppresses native manifests and unloaded owners when their flags are off 验证了这一点。affectedCandidates()静态受影响包推断affectedCandidates(files)接收工作区相对路径的变更文件列表返回{ packages, conservative }packages可能受影响的包含传递输入依赖者conservative当元数据不完整、自定义全局输入命中或拓扑变更导致需要返回全部包时为true。源码static_workspace.rs 的affected_candidates展示了完整的处理流程路径归一化与校验normalize_paths拒绝绝对路径与逃逸工作区根的路径如../outside.js、apps/../../outside.js任何一条非法路径都会使整个调用 reject空列表短路空变更列表直接返回{ packages: [], conservative: false }保守回退判定当affectednessComplete为 false、全局输入命中global_inputs_changed见 static_dependencies.rs或存在拓扑变更文件时返回全部包并置conservative: true原生种子分离seeds_for_path为每个路径找到原生工作区级种子如 Cargo.lock 变更会失效对应工作区普通源码路径走共享的 JavaScript 包映射器传递闭包计算通过affected_by_with_inputs在静态输入关系上求受影响闭包再与清单取交集。支持的静态依赖关系官方文档给出了静态分析覆盖的细节Cargo path dependencies包括workspace 继承workspace inheritance如[workspace.dependencies]与{ workspace true }别名aliases如renamed { package core, path ... }build/dev 依赖输入[build-dependencies]、[dev-dependencies]可选特性optional features每个 target 条件分支如[target.cfg(windows).build-dependencies]。uv 输入包括workspace sources[tool.uv.sources]中的{ workspace true }指向工作区成员的本地路径{ path ... }归一化后的名称如Py.Core与Py_Core的规范化映射extras 与依赖组dependency groups如[dependency-groups]条件源选择的并集带 marker 的多源分支。此外成员源覆盖member source overrides优先于根源覆盖共置包作用域会在生态之间传播变更例如同一目录下的 Rust 包与 JS 包互连。全程不运行任何编译器、解析器或语言命令。测试 selectively propagates Cargo and uv dependencies without language tools 给出了非常直观的断言示例修改crates/core/src/deleted.rs会选中[core, rust-app]修改python/shared/deleted.py会选中[py-api, py-core]修改Cargo.lock只失效[core, rust-app]而修改Cargo.toml、pyproject.toml等拓扑清单则会回退为全部包且conservative: true。源码变更的选择语义官方文档用一句话概括了核心语义Source changes select their package owners and transitive dependents.即源码变更会选中其包所有者及其传递依赖者。例如Rust 库的变更会选中其 Rust 消费者而不会选中无关的 Python 或 JavaScript 包。原生工作区 lockfileCargo.lock、uv.lock与工具链配置变更会使对应原生工作区及其依赖者失效。清单编辑manifest edits在任何生态中都会选中全部包嵌套 JavaScript lockfile 编辑同样如此因为被删除/改名的作用域可能抹去历史上的跨语言关系包括通过共置包作用域建立的连接。从源码看is_topology_changestatic_workspace.rs将Cargo.toml、pyproject.toml以及非根的package.json、package-lock.json、pnpm-lock.yaml、pnpm-workspace.yaml、yarn.lock、bun.lock、bun.lockb都视为拓扑变更。保守回退conservative fallback的场景无法解析的情况会返回全部包并置conservative: true。官方文档列举的场景包括不支持的贡献者目前是 GoCargopatch/replace或 source/path overrides非工作区成员的本地依赖Python 动态依赖dynamic [dependencies]或依赖元数据覆盖如uv.toml的override-dependencies显式的 Maturin manifest-path 配置。测试 reports unresolved static dependencies 逐条覆盖了上述场景并断言此时affectednessComplete false。文档同时提醒应检查affectednessComplete而不要假定每个未加载的工具链都是未解析状态。全局输入global inputs如turbo.json的globalDependencies或global.inputs只有在变更路径匹配时才触发回退排除模式以!开头的模式被保守地忽略不支持的 glob 模式回退为全部包。global_inputs_changed的实现static_dependencies.rs只匹配包含性模式且对含$的变量模式与无法解析的 glob 一律按命中处理从而避免漏报。边界情况与部署注意事项官方文档对本 API 的适用范围给出了严格边界理解这些边界对正确使用至关重要这些是仓库声明的包输入不是任意的构建脚本读取build-script reads、外部依赖解析、任务排序或精确的 target/feature 选择空变更列表恒返回{ packages: [], conservative: false }变更路径必须相对于工作区根并使用当前系统的路径分隔符绝对路径与逃逸仓库的路径会被拒绝normalize_paths会检查绝对路径与..逃逸清单描述的是当前 checkout不包含旧版本中已删除的包部署消费方必须把清单中缺失的已配置项目根视为未知/受影响而不能把“缺失”当作安全跳过畸形清单会使发现过程整体失败reject部署消费方应同样对这些错误采用“开放失败”fail open策略。测试同样覆盖了这些行为rejects absolute and escaping change paths 验证非法路径 rejectrejects malformed manifest 验证损坏的package.json、Cargo.toml、pyproject.toml会让findPackages()拒绝执行。与 Workspace 的取舍何时使用哪一个官方文档明确指出StaticWorkspace没有完整的图graph、任务或 lockfile 方法——findPackagesWithGraph、findPackageByPath、affectedPackages、lockfilePackages、packagesFromLockfile、tasks等都不存在。测试中对此逐方法做了断言。因此选择原则是需要权威依赖精度完整依赖图、任务、lockfile 闭包时使用Workspace完整的原生发现仍然依赖工具链Workspace.find会尝试调用 cargo 等获取权威元数据只需要包清单与保守受影响推断、且希望完全不触碰语言工具链时使用StaticWorkspace现有的Workspace.find()及其 lockfile-only 的skipPackageGraph选项保持不变二者行为不受StaticWorkspace影响。测试 leaves default full discovery authoritative and skipPackageGraph lockfile-only 还验证了一个重要差异Workspace.find(root, { skipPackageGraph: true })不会调用任何工具而默认的Workspace.find(root)会尝试调用 cargo 并在失败时 reject。无工具保证的验证方式隔离环境测试如何证明“不调用语言工具”static-discovery.test.ts给出了一套严谨的方法测试在临时夹具中运行隔离的 Node 子进程从环境变量中剔除PATH、NODE_OPTIONS、CARGO、RUSTC、UV、PYTHON、GO、GOROOT等所有相关变量并用三种 PATH 模式验证absent完全移除 PATHemptyPATH 指向空目录trap在 PATH 中放置 cargo/rustc/uv/python/go 等可执行陷阱脚本一旦被调用就写日志并退出码 97。测试断言无论哪种模式StaticWorkspace.find、findPackages、affectedCandidates都能正常工作且工具调用日志始终为空static discovery must never invoke a language tool。这从测试层面证明了静态发现的“无子进程”承诺。该测试还覆盖了find在嵌套目录如apps/web中向上推断工作区根的行为与Workspace.find(undefined, { skipPackageGraph: true })得到相同根目录。从源码看 StaticWorkspace 的实现结构为便于读者深入阅读这里给出静态发现功能在仓库中的主要实现位置JS 绑定入口packages/turbo-repository/js/index.js 通过 NAPI-RS 生成的加载器按平台/架构选择.node模块并导出StaticWorkspace、Workspace、Package、PackageDetails、PackageManager类型声明packages/turbo-repository/js/index.d.ts由 Rust 的 N-API 声明自动生成StaticWorkspace 实现packages/turbo-repository/rust/src/static_workspace.rs包括find工厂、三个状态 getter、inventory()、affected_candidates()以及路径归一化与拓扑变更判定发现模式接线packages/turbo-repository/rust/src/internal.rs其中DiscoveryMode::{Full, SkipPackageGraph, Static}区分三种模式三个experimental*flags 通过PackageGraphBuilder::with_cargo()/with_uv()/with_go()接入Static 模式使用build_lazy()惰性构建图并计算静态受影响知识静态依赖核心crates/turborepo-repository/src/static_dependencies.rs包含StaticPackageDependencies、StaticAffectedness、global_inputs_changed、seeds_for_path、affected_by含共置包双向输入边工具链与发现基础设施crates/turborepo-repository/src/toolchain.rs、crates/turborepo-repository/src/discovery.rs以及 Cargo/uv/Go 各自的解析逻辑crates/turborepo-repository/src/cargo.rs、crates/turborepo-repository/src/uv.rs、crates/turborepo-repository/src/go.rs测试与夹具packages/turbo-repository/tests/static-discovery.test.ts 与 packages/turbo-repository/tests/fixtures/polyglot-monorepo同时含 npm、Cargo、uv、go.work 的混合仓库。开发与维护注意事项官方文档最后给出了两条面向维护者的说明js/index.d.ts是检入checked in的并在开发构建时从 Rust 的 N-API 声明重新生成修改接口时必须同步更新 Rust API 文档并提交重新生成的声明文件如果新增导出index.js也需要同步更新例如在module.exports中补充新的导出项。总结StaticWorkspace为多语言 monorepo 提供了一条“零工具链”的分析路径它复用 Turborepo 既有的 JavaScript 工作区推断与experimentalCargoWorkspaces、experimentalPythonWorkspaces、experimentalGoWorkspaces三个 future flags以纯清单解析的方式产出确定性的包清单与保守的受影响闭包。其“宁可多报、不可漏报”的设计保守回退、畸形清单拒绝、清单编辑与全局输入触发全量使其特别适合部署流水线中的变更影响评估、依赖审计与缓存失效判定。当需要权威依赖图、任务与 lockfile 闭包时则仍应使用Workspace。【免费下载链接】turboBuild system optimized for JavaScript and TypeScript, written in Rust项目地址: https://gitcode.com/gh_mirrors/tu/turbo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考