Skip to main content

dev_prune/adapters/
pnpm.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// PNPM adapter implementation.
5
6use super::{BloatDir, EnforcePolicy, PackageManager, dir_size, enforce_two_tier, run_command};
7use anyhow::Result;
8use std::path::Path;
9
10/// PNPM package manager adapter.
11pub struct Pnpm;
12
13impl PackageManager for Pnpm {
14    /// Returns the name of the package manager.
15    fn name(&self) -> &'static str {
16        "pnpm"
17    }
18
19    /// Detects if the project uses pnpm by checking for `pnpm-lock.yaml`.
20    fn detect(&self, project_dir: &Path) -> bool {
21        project_dir.join("pnpm-lock.yaml").exists()
22    }
23
24    /// Returns the bloat directories for pnpm (node_modules).
25    fn bloat_dirs(&self, project_dir: &Path) -> Vec<BloatDir> {
26        let node_modules = project_dir.join("node_modules");
27        if node_modules.exists() {
28            let size = dir_size(&node_modules);
29            vec![BloatDir {
30                name: "node_modules".to_string(),
31                path: node_modules,
32                size_bytes: size,
33            }]
34        } else {
35            vec![]
36        }
37    }
38
39    /// Enforces the lockfile without installing anything, and without writing.
40    ///
41    /// `--lockfile-only` resolves `package.json` and touches no `node_modules`, but on
42    /// its own it *writes* the resolution back to `pnpm-lock.yaml`. `--frozen-lockfile`
43    /// turns that write into a failure, which is the answer we actually want: a lockfile
44    /// that no longer matches the manifest cannot rebuild the tree we are about to
45    /// delete, so the prune should be refused rather than the file quietly fixed.
46    fn enforce_lockfile(&self, project_dir: &Path, policy: EnforcePolicy) -> Result<()> {
47        let lockfile = project_dir.join("pnpm-lock.yaml");
48        enforce_two_tier(
49            &lockfile,
50            "pnpm",
51            &["install", "--lockfile-only", "--frozen-lockfile"],
52            &["install", "--lockfile-only"],
53            project_dir,
54            policy,
55        )
56    }
57
58    /// Restores the dependencies using the lockfile.
59    fn restore(&self, project_dir: &Path) -> Result<()> {
60        run_command("pnpm", &["install", "--frozen-lockfile"], project_dir)
61    }
62
63    fn lockfiles(&self) -> &'static [&'static str] {
64        &["pnpm-lock.yaml"]
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use std::fs;
72    use tempfile::tempdir;
73
74    #[test]
75    fn test_name() {
76        assert_eq!(Pnpm.name(), "pnpm");
77    }
78
79    #[test]
80    fn test_detect_positive() {
81        let dir = tempdir().unwrap();
82        fs::File::create(dir.path().join("pnpm-lock.yaml")).unwrap();
83        assert!(Pnpm.detect(dir.path()));
84    }
85
86    #[test]
87    fn test_detect_negative() {
88        let dir = tempdir().unwrap();
89        assert!(!Pnpm.detect(dir.path()));
90    }
91
92    #[test]
93    fn test_bloat_dirs_present() {
94        let dir = tempdir().unwrap();
95        fs::create_dir(dir.path().join("node_modules")).unwrap();
96        let bloat = Pnpm.bloat_dirs(dir.path());
97        assert_eq!(bloat.len(), 1);
98        assert_eq!(bloat[0].path, dir.path().join("node_modules"));
99    }
100
101    #[test]
102    fn test_bloat_dirs_absent() {
103        let dir = tempdir().unwrap();
104        let bloat = Pnpm.bloat_dirs(dir.path());
105        assert!(bloat.is_empty());
106    }
107}