Skip to main content

dev_prune/adapters/
yarn.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Yarn adapter implementation.
5
6use super::{BloatDir, EnforcePolicy, PackageManager, dir_size, enforce_two_tier, run_command};
7use anyhow::Result;
8use std::path::Path;
9
10/// Yarn package manager adapter.
11pub struct Yarn;
12
13impl PackageManager for Yarn {
14    /// Returns the name of the package manager.
15    fn name(&self) -> &'static str {
16        "yarn"
17    }
18
19    /// Detects if the project uses yarn by checking for `yarn.lock`.
20    fn detect(&self, project_dir: &Path) -> bool {
21        project_dir.join("yarn.lock").exists()
22    }
23
24    /// Returns the bloat directories for yarn (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.
40    ///
41    /// `--mode update-lockfile` resolves `package.json` and writes only `yarn.lock`.
42    /// It is a Yarn Berry (2+) flag; Yarn Classic rejects it. Classic offers no
43    /// resolve-only mode at all — its nearest equivalent, `yarn install
44    /// --frozen-lockfile`, performs a full install and runs every dependency's
45    /// lifecycle scripts, which is not something to do as a precondition for deleting
46    /// that same tree. So on Classic an existing `yarn.lock` is itself the proof that
47    /// `node_modules` is rebuildable, and that is what we require.
48    ///
49    /// On Berry, `--immutable` is what keeps the resolution read-only: it fails when
50    /// `yarn.lock` would change rather than writing the change out.
51    fn enforce_lockfile(&self, project_dir: &Path, policy: EnforcePolicy) -> Result<()> {
52        let lockfile = project_dir.join("yarn.lock");
53        let berry = enforce_two_tier(
54            &lockfile,
55            "yarn",
56            &["install", "--immutable", "--mode", "update-lockfile"],
57            &["install", "--mode", "update-lockfile"],
58            project_dir,
59            policy,
60        );
61        if berry.is_err() && !lockfile.exists() {
62            anyhow::bail!(
63                "`yarn install --mode update-lockfile` failed and there is no \
64                 `yarn.lock` to fall back on. Cannot prove `node_modules` is \
65                 rebuildable — run `yarn install` and commit the lockfile first."
66            );
67        }
68        Ok(())
69    }
70
71    /// Restores the dependencies using the lockfile.
72    fn restore(&self, project_dir: &Path) -> Result<()> {
73        run_command("yarn", &["install", "--immutable"], project_dir)
74    }
75
76    fn lockfiles(&self) -> &'static [&'static str] {
77        &["yarn.lock"]
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use std::fs;
85    use tempfile::tempdir;
86
87    #[test]
88    fn test_name() {
89        assert_eq!(Yarn.name(), "yarn");
90    }
91
92    #[test]
93    fn test_detect_positive() {
94        let dir = tempdir().unwrap();
95        fs::File::create(dir.path().join("yarn.lock")).unwrap();
96        assert!(Yarn.detect(dir.path()));
97    }
98
99    #[test]
100    fn test_detect_negative() {
101        let dir = tempdir().unwrap();
102        assert!(!Yarn.detect(dir.path()));
103    }
104
105    #[test]
106    fn test_bloat_dirs_present() {
107        let dir = tempdir().unwrap();
108        fs::create_dir(dir.path().join("node_modules")).unwrap();
109        let bloat = Yarn.bloat_dirs(dir.path());
110        assert_eq!(bloat.len(), 1);
111        assert_eq!(bloat[0].path, dir.path().join("node_modules"));
112    }
113
114    #[test]
115    fn test_bloat_dirs_absent() {
116        let dir = tempdir().unwrap();
117        let bloat = Yarn.bloat_dirs(dir.path());
118        assert!(bloat.is_empty());
119    }
120}