plans/refactors/package-manager-resolving.md
Notes on a future refactor motivated by nub support and the
PackageManager::Nub { lockfile } wrapper introduced in #13120.
PackageManager currently conflates several concerns:
package.json (packageManager)npm, pnpm, nub, …)pnpm-workspace.yaml, default exclusionsEvery supported manager except nub maps 1:1 across these axes. nub splits them:
nubThe current fix is a wrapper variant on the existing enum:
Nub {
lockfile: Box<PackageManager>, // concrete backend, never nested Nub
}
This works but leaks complexity: call sites must know whether to use the outer
identity or the inner lockfile backend, and behavior routing is inconsistent
(some methods delegate, some special-case Nub, some use lockfile_manager()).
underlying_lockfile_manager(repo_root) probes disk:
bun → pnpm → yarn → npm (default).| Helper | Purpose |
|---|---|
lockfile_manager() | Peel Nub to the concrete lockfile backend |
is_pnpm_family() | Predicate for pnpm lockfile semantics via lockfile_manager() |
with_resolved_nub_lockfile(repo_root) | Re-probe disk after daemon proto round-trip |
| Operation | Route |
|---|---|
command(), name() | Outer nub identity |
read_lockfile, parse_lockfile, prune_patched_packages | Delegate to inner lockfile |
read_catalogs, is_pnpm_family (external) | Via lockfile_manager() |
arg_separator | Outer nub (pnpm-compatible CLI) |
get_default_exclusions | Outer nub (npm-style) |
get_configured_workspace_globs | Hybrid: pnpm-workspace.yaml if underlying is pnpm |
Nub arm when adding new
PackageManager behavior.lockfile is resolved at detection time; not automatically
refreshed if lockfiles change without re-discovery.Nub = 7 only; underlying type is lost
on the wire and must be re-resolved from disk on the client.Box<PackageManager> inside PackageManager is a smell
that the enum is doing composition without a composition model.supported_managers() — intentionally excludes nub (no lockfile of its
own); lockfile change detection relies on iterating known lockfile names.Split identity from lockfile backend explicitly:
struct ResolvedPackageManager {
/// What the user declared and what we execute.
identity: PackageManagerIdentity,
/// Always concrete: Npm | Pnpm | Pnpm6 | Pnpm9 | Yarn | Berry | Bun.
/// Never Nub.
lockfile_backend: LockfileBackend,
}
enum PackageManagerIdentity {
Npm,
Pnpm,
Yarn,
Bun,
Nub,
// ...
}
Or a trait-based split:
trait TaskExecutor {
fn binary(&self) -> &str;
fn arg_separator(&self, user_args: &[impl AsRef<str>]) -> Option<&str>;
}
trait LockfileProvider {
fn read_lockfile(&self, root: &AbsoluteSystemPath, pkg: &PackageJson)
-> Result<Box<dyn Lockfile>, Error>;
fn lockfile_name(&self) -> &str;
}
trait WorkspaceDiscoverer {
fn get_workspace_globs(&self, root: &AbsoluteSystemPath)
-> Result<WorkspaceGlobs, Error>;
}
nub would implement TaskExecutor as nub and LockfileProvider /
WorkspaceDiscoverer by forwarding to lockfile_backend.
ResolvedPackageManager alongside the existing enum; populate
both during detection. No call-site changes yet.LockfileBackend (or lockfile_backend field
accessors). Update PackageGraph, prune, cache hashing to use
resolved.lockfile_backend instead of matching on PackageManager.identity. Task executor uses
resolved.identity.binary() only.PackageManager::Nub { lockfile } once all call sites use the
split struct.lockfile_backend or document that identity-only wire values require
disk re-probe.Nub match arms / delegation gaps.crates/turborepo-repository/src/package_manager/mod.rs — enum, helpers,
delegation match armscrates/turborepo-repository/src/package_manager/nub.rs — underlying lockfile
resolutioncrates/turborepo-repository/src/package_graph/builder.rs —
with_resolved_nub_lockfile after discoverycrates/turborepo-lib/src/run/package_discovery/mod.rs — daemon client
re-resolutioncrates/turborepo-daemon/src/proto/turbod.proto — Nub = 7 wire valuecrates/turborepo-lib/src/commands/prune.rs — is_pnpm_family() usagelockfile_backend, or should nub have
fixed opinions (current hybrid for pnpm-workspace.yaml)?packages/turbo-workspaces) share a single
SUPPORTED_PACKAGE_MANAGERS list with Rust via codegen or a shared JSON
schema?