Skip to main content

dev_prune/adapters/
bun.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Bun adapter implementation.
5
6use super::{
7    BloatDir, EnforcePolicy, PackageManager, dir_size, lock_sync_or_verify_with_timeout,
8    run_command,
9};
10use anyhow::Result;
11use std::path::Path;
12
13/// Bun package manager adapter.
14pub struct Bun;
15
16impl PackageManager for Bun {
17    /// Returns the name of the package manager.
18    fn name(&self) -> &'static str {
19        "bun"
20    }
21
22    /// Detects if the project uses bun by checking for `bun.lockb` or `bun.lock`.
23    fn detect(&self, project_dir: &Path) -> bool {
24        project_dir.join("bun.lockb").exists() || project_dir.join("bun.lock").exists()
25    }
26
27    /// Returns the bloat directories for bun (node_modules).
28    fn bloat_dirs(&self, project_dir: &Path) -> Vec<BloatDir> {
29        let node_modules = project_dir.join("node_modules");
30        if node_modules.exists() {
31            let size = dir_size(&node_modules);
32            vec![BloatDir {
33                name: "node_modules".to_string(),
34                path: node_modules,
35                size_bytes: size,
36            }]
37        } else {
38            vec![]
39        }
40    }
41
42    /// Verifies the lockfile resolves cleanly, without installing anything.
43    ///
44    /// `--dry-run` is essential, not cosmetic. A plain `bun install --frozen-lockfile`
45    /// is a real install: it downloads every dependency and runs their lifecycle
46    /// scripts — third-party code executed as a precondition for *deleting* the very
47    /// tree it just built. With `--dry-run`, bun still resolves `package.json` against
48    /// the lockfile and fails when they disagree, but writes nothing.
49    ///
50    /// bun is the one manager whose natural check was already read-only, so there is no
51    /// writing form to opt into and `allow_rewrite` changes nothing here.
52    fn enforce_lockfile(&self, project_dir: &Path, policy: EnforcePolicy) -> Result<()> {
53        let lockfile = if project_dir.join("bun.lockb").exists() {
54            project_dir.join("bun.lockb")
55        } else {
56            project_dir.join("bun.lock")
57        };
58        lock_sync_or_verify_with_timeout(
59            &lockfile,
60            "bun",
61            &[
62                "install",
63                "--frozen-lockfile",
64                "--dry-run",
65                "--ignore-scripts",
66            ],
67            project_dir,
68            policy.timeout,
69        )
70    }
71
72    /// Restores the dependencies using the lockfile.
73    fn restore(&self, project_dir: &Path) -> Result<()> {
74        run_command("bun", &["install", "--frozen-lockfile"], project_dir)
75    }
76
77    fn lockfiles(&self) -> &'static [&'static str] {
78        &["bun.lockb", "bun.lock"]
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use std::fs;
86    use tempfile::tempdir;
87
88    #[test]
89    fn test_name() {
90        assert_eq!(Bun.name(), "bun");
91    }
92
93    #[test]
94    fn test_detect_positive_lockb() {
95        let dir = tempdir().unwrap();
96        fs::File::create(dir.path().join("bun.lockb")).unwrap();
97        assert!(Bun.detect(dir.path()));
98    }
99
100    #[test]
101    fn test_detect_positive_lock() {
102        let dir = tempdir().unwrap();
103        fs::File::create(dir.path().join("bun.lock")).unwrap();
104        assert!(Bun.detect(dir.path()));
105    }
106
107    #[test]
108    fn test_detect_negative() {
109        let dir = tempdir().unwrap();
110        assert!(!Bun.detect(dir.path()));
111    }
112
113    #[test]
114    fn test_bloat_dirs_present() {
115        let dir = tempdir().unwrap();
116        fs::create_dir(dir.path().join("node_modules")).unwrap();
117        let bloat = Bun.bloat_dirs(dir.path());
118        assert_eq!(bloat.len(), 1);
119        assert_eq!(bloat[0].path, dir.path().join("node_modules"));
120    }
121
122    #[test]
123    fn test_bloat_dirs_absent() {
124        let dir = tempdir().unwrap();
125        let bloat = Bun.bloat_dirs(dir.path());
126        assert!(bloat.is_empty());
127    }
128}