Skip to main content

changepacks_node/
lib.rs

1//! # changepacks-node
2//!
3//! Node.js project support for changepacks.
4//!
5//! Implements project discovery, version management, and workspace detection for package.json
6//! files. Automatically detects the package manager (npm, pnpm, yarn, bun) by looking for
7//! lock files and provides appropriate publish commands for each.
8
9pub mod finder;
10pub mod package;
11pub mod workspace;
12
13pub use finder::NodeProjectFinder;
14
15use std::path::Path;
16
17/// Represents the detected Node.js package manager
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum PackageManager {
20    Npm,
21    Yarn,
22    Pnpm,
23    Bun,
24}
25
26impl PackageManager {
27    /// Returns the publish command for this package manager
28    #[must_use]
29    pub fn publish_command(&self) -> &'static str {
30        match self {
31            Self::Npm => "npm publish",
32            Self::Yarn => "yarn npm publish",
33            Self::Pnpm => "pnpm publish",
34            Self::Bun => "bun publish",
35        }
36    }
37}
38
39/// Detects the package manager by checking for lock files in the given directory
40/// Priority: bun.lockb > pnpm-lock.yaml > yarn.lock > package-lock.json > npm (default)
41#[must_use]
42pub fn detect_package_manager(dir: &Path) -> PackageManager {
43    if dir.join("bun.lockb").exists() || dir.join("bun.lock").exists() {
44        PackageManager::Bun
45    } else if dir.join("pnpm-lock.yaml").exists() {
46        PackageManager::Pnpm
47    } else if dir.join("yarn.lock").exists() {
48        PackageManager::Yarn
49    } else if dir.join("package-lock.json").exists() {
50        PackageManager::Npm
51    } else {
52        // Default to npm if no lock file found
53        PackageManager::Npm
54    }
55}
56
57/// Detects the package manager by searching from the given path up to the root
58#[must_use]
59pub fn detect_package_manager_recursive(path: &Path) -> PackageManager {
60    let mut current = if path.is_file() {
61        path.parent()
62    } else {
63        Some(path)
64    };
65
66    while let Some(dir) = current {
67        let pm = detect_package_manager(dir);
68        if pm != PackageManager::Npm || dir.join("package-lock.json").exists() {
69            return pm;
70        }
71        current = dir.parent();
72    }
73
74    PackageManager::Npm
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use std::fs;
81    use tempfile::TempDir;
82
83    #[test]
84    fn test_detect_bun_lockb() {
85        let temp_dir = TempDir::new().unwrap();
86        fs::write(temp_dir.path().join("bun.lockb"), "").unwrap();
87        assert_eq!(detect_package_manager(temp_dir.path()), PackageManager::Bun);
88    }
89
90    #[test]
91    fn test_detect_bun_lock() {
92        let temp_dir = TempDir::new().unwrap();
93        fs::write(temp_dir.path().join("bun.lock"), "").unwrap();
94        assert_eq!(detect_package_manager(temp_dir.path()), PackageManager::Bun);
95    }
96
97    #[test]
98    fn test_detect_pnpm() {
99        let temp_dir = TempDir::new().unwrap();
100        fs::write(temp_dir.path().join("pnpm-lock.yaml"), "").unwrap();
101        assert_eq!(
102            detect_package_manager(temp_dir.path()),
103            PackageManager::Pnpm
104        );
105    }
106
107    #[test]
108    fn test_detect_yarn() {
109        let temp_dir = TempDir::new().unwrap();
110        fs::write(temp_dir.path().join("yarn.lock"), "").unwrap();
111        assert_eq!(
112            detect_package_manager(temp_dir.path()),
113            PackageManager::Yarn
114        );
115    }
116
117    #[test]
118    fn test_detect_npm() {
119        let temp_dir = TempDir::new().unwrap();
120        fs::write(temp_dir.path().join("package-lock.json"), "{}").unwrap();
121        assert_eq!(detect_package_manager(temp_dir.path()), PackageManager::Npm);
122    }
123
124    #[test]
125    fn test_detect_default_npm() {
126        let temp_dir = TempDir::new().unwrap();
127        assert_eq!(detect_package_manager(temp_dir.path()), PackageManager::Npm);
128    }
129
130    #[test]
131    fn test_bun_priority_over_others() {
132        let temp_dir = TempDir::new().unwrap();
133        fs::write(temp_dir.path().join("bun.lockb"), "").unwrap();
134        fs::write(temp_dir.path().join("pnpm-lock.yaml"), "").unwrap();
135        fs::write(temp_dir.path().join("yarn.lock"), "").unwrap();
136        assert_eq!(detect_package_manager(temp_dir.path()), PackageManager::Bun);
137    }
138
139    #[test]
140    fn test_pnpm_priority_over_yarn() {
141        let temp_dir = TempDir::new().unwrap();
142        fs::write(temp_dir.path().join("pnpm-lock.yaml"), "").unwrap();
143        fs::write(temp_dir.path().join("yarn.lock"), "").unwrap();
144        assert_eq!(
145            detect_package_manager(temp_dir.path()),
146            PackageManager::Pnpm
147        );
148    }
149
150    #[test]
151    fn test_publish_commands() {
152        assert_eq!(PackageManager::Npm.publish_command(), "npm publish");
153        assert_eq!(PackageManager::Yarn.publish_command(), "yarn npm publish");
154        assert_eq!(PackageManager::Pnpm.publish_command(), "pnpm publish");
155        assert_eq!(PackageManager::Bun.publish_command(), "bun publish");
156    }
157
158    #[test]
159    fn test_detect_recursive() {
160        let temp_dir = TempDir::new().unwrap();
161        let sub_dir = temp_dir.path().join("packages").join("core");
162        fs::create_dir_all(&sub_dir).unwrap();
163        fs::write(temp_dir.path().join("pnpm-lock.yaml"), "").unwrap();
164        fs::write(sub_dir.join("package.json"), "{}").unwrap();
165
166        assert_eq!(
167            detect_package_manager_recursive(&sub_dir.join("package.json")),
168            PackageManager::Pnpm
169        );
170    }
171}