dev_prune/adapters/
npm.rs1use super::{BloatDir, EnforcePolicy, PackageManager, dir_size, enforce_two_tier, run_command};
7use anyhow::Result;
8use std::path::Path;
9
10pub struct Npm;
12
13impl PackageManager for Npm {
14 fn name(&self) -> &'static str {
16 "npm"
17 }
18
19 fn detect(&self, project_dir: &Path) -> bool {
21 project_dir.join("package-lock.json").exists()
22 }
23
24 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 fn enforce_lockfile(&self, project_dir: &Path, policy: EnforcePolicy) -> Result<()> {
47 let lockfile = project_dir.join("package-lock.json");
48 enforce_two_tier(
49 &lockfile,
50 "npm",
51 &["ci", "--dry-run", "--ignore-scripts"],
52 &["install", "--package-lock-only", "--ignore-scripts"],
53 project_dir,
54 policy,
55 )
56 }
57
58 fn restore(&self, project_dir: &Path) -> Result<()> {
60 run_command("npm", &["ci"], project_dir)
61 }
62
63 fn lockfiles(&self) -> &'static [&'static str] {
64 &["package-lock.json"]
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!(Npm.name(), "npm");
77 }
78
79 #[test]
85 fn a_default_pass_never_rewrites_a_stale_lockfile() {
86 if !super::super::binary_available("npm") {
87 return;
88 }
89 let dir = tempdir().unwrap();
90 fs::write(
91 dir.path().join("package.json"),
92 r#"{"name":"stale","version":"1.0.0","dependencies":{"left-pad":"^1.3.0"}}"#,
93 )
94 .unwrap();
95
96 let stale = r#"{"name":"stale","version":"1.0.0","lockfileVersion":3,"requires":true,"packages":{"":{"name":"stale","version":"1.0.0"}}}"#;
98 fs::write(dir.path().join("package-lock.json"), stale).unwrap();
99
100 let result = Npm.enforce_lockfile(dir.path(), EnforcePolicy::default());
101
102 assert!(
103 result.is_err(),
104 "a lockfile out of sync with package.json must not pass verification"
105 );
106 assert_eq!(
107 fs::read_to_string(dir.path().join("package-lock.json")).unwrap(),
108 stale,
109 "the read-only verification rewrote package-lock.json"
110 );
111 }
112
113 #[test]
114 fn test_detect_positive() {
115 let dir = tempdir().unwrap();
116 fs::File::create(dir.path().join("package-lock.json")).unwrap();
117 assert!(Npm.detect(dir.path()));
118 }
119
120 #[test]
121 fn test_detect_negative() {
122 let dir = tempdir().unwrap();
123 assert!(!Npm.detect(dir.path()));
124 }
125
126 #[test]
127 fn test_bloat_dirs_present() {
128 let dir = tempdir().unwrap();
129 fs::create_dir(dir.path().join("node_modules")).unwrap();
130 let bloat = Npm.bloat_dirs(dir.path());
131 assert_eq!(bloat.len(), 1);
132 assert_eq!(bloat[0].path, dir.path().join("node_modules"));
133 }
134
135 #[test]
136 fn test_bloat_dirs_absent() {
137 let dir = tempdir().unwrap();
138 let bloat = Npm.bloat_dirs(dir.path());
139 assert!(bloat.is_empty());
140 }
141}