cuenv_workspaces/materializer/
node_modules.rs1use super::Materializer;
4use crate::core::types::{LockfileEntry, PackageManager, Workspace};
5use crate::error::{Error, Result};
6use std::path::{Path, PathBuf};
7
8#[cfg(unix)]
9use std::os::unix::fs::symlink;
10#[cfg(windows)]
11use std::os::windows::fs::symlink_dir as symlink;
12
13pub struct NodeModulesMaterializer;
15
16impl Materializer for NodeModulesMaterializer {
17 fn materialize(
18 &self,
19 workspace: &Workspace,
20 _entries: &[LockfileEntry],
21 target_dir: &Path,
22 ) -> Result<()> {
23 if !matches!(
24 workspace.manager,
25 PackageManager::Npm
26 | PackageManager::Bun
27 | PackageManager::Pnpm
28 | PackageManager::YarnClassic
29 | PackageManager::YarnModern
30 ) {
31 return Ok(());
32 }
33
34 let workspace_nm = workspace.root.join("node_modules");
41 let target_nm = target_dir.join("node_modules");
42
43 if workspace_nm.exists() {
44 if !target_nm.exists() {
45 symlink(&workspace_nm, &target_nm).map_err(|e| Error::Io {
46 source: e,
47 path: Some(target_nm.clone()),
48 operation: "symlink node_modules".to_string(),
49 })?;
50 }
51 } else {
52 }
56
57 Ok(())
58 }
59}
60
61impl NodeModulesMaterializer {
62 pub fn detect_cache_dir(manager: PackageManager) -> Option<PathBuf> {
66 let home = std::env::var_os("HOME")
67 .or_else(|| std::env::var_os("USERPROFILE"))
68 .map(PathBuf::from)?;
69
70 match manager {
71 PackageManager::Npm => Some(home.join(".npm")),
72 PackageManager::Pnpm => {
73 Some(home.join(".local/share/pnpm/store/v3"))
75 }
76 PackageManager::Bun => Some(home.join(".bun/install/cache")),
77 PackageManager::YarnClassic => Some(home.join(".yarn/cache")),
78 PackageManager::YarnModern => Some(home.join(".yarn/berry/cache")),
79 PackageManager::Deno => Some(home.join(".cache/deno")),
80 PackageManager::Cargo => None,
81 }
82 }
83}
84
85#[cfg(test)]
86#[allow(unsafe_code)]
87mod tests {
88 use super::*;
89 use std::fs;
90 use tempfile::TempDir;
91
92 fn make_workspace(root: &Path, manager: PackageManager) -> Workspace {
93 Workspace::new(root.to_path_buf(), manager)
94 }
95
96 #[test]
101 fn test_node_modules_materializer_skips_cargo() {
102 let temp_dir = TempDir::new().unwrap();
103 let workspace = make_workspace(temp_dir.path(), PackageManager::Cargo);
104 let target_dir = temp_dir.path().join("hermetic");
105 fs::create_dir_all(&target_dir).unwrap();
106
107 let materializer = NodeModulesMaterializer;
108 let result = materializer.materialize(&workspace, &[], &target_dir);
109
110 assert!(result.is_ok());
111 assert!(!target_dir.join("node_modules").exists());
113 }
114
115 #[test]
116 fn test_node_modules_materializer_handles_npm() {
117 let temp_dir = TempDir::new().unwrap();
118 let workspace = make_workspace(temp_dir.path(), PackageManager::Npm);
119 let target_dir = temp_dir.path().join("hermetic");
120 fs::create_dir_all(&target_dir).unwrap();
121
122 let workspace_nm = temp_dir.path().join("node_modules");
124 fs::create_dir_all(&workspace_nm).unwrap();
125 fs::write(workspace_nm.join("marker"), "npm").unwrap();
126
127 let materializer = NodeModulesMaterializer;
128 let result = materializer.materialize(&workspace, &[], &target_dir);
129
130 assert!(result.is_ok());
131 assert!(target_dir.join("node_modules").exists());
133 }
134
135 #[test]
136 fn test_node_modules_materializer_handles_bun() {
137 let temp_dir = TempDir::new().unwrap();
138 let workspace = make_workspace(temp_dir.path(), PackageManager::Bun);
139 let target_dir = temp_dir.path().join("hermetic");
140 fs::create_dir_all(&target_dir).unwrap();
141
142 let workspace_nm = temp_dir.path().join("node_modules");
144 fs::create_dir_all(&workspace_nm).unwrap();
145
146 let materializer = NodeModulesMaterializer;
147 let result = materializer.materialize(&workspace, &[], &target_dir);
148
149 assert!(result.is_ok());
150 assert!(target_dir.join("node_modules").exists());
151 }
152
153 #[test]
154 fn test_node_modules_materializer_handles_pnpm() {
155 let temp_dir = TempDir::new().unwrap();
156 let workspace = make_workspace(temp_dir.path(), PackageManager::Pnpm);
157 let target_dir = temp_dir.path().join("hermetic");
158 fs::create_dir_all(&target_dir).unwrap();
159
160 let workspace_nm = temp_dir.path().join("node_modules");
162 fs::create_dir_all(&workspace_nm).unwrap();
163
164 let materializer = NodeModulesMaterializer;
165 let result = materializer.materialize(&workspace, &[], &target_dir);
166
167 assert!(result.is_ok());
168 assert!(target_dir.join("node_modules").exists());
169 }
170
171 #[test]
172 fn test_node_modules_materializer_handles_yarn_classic() {
173 let temp_dir = TempDir::new().unwrap();
174 let workspace = make_workspace(temp_dir.path(), PackageManager::YarnClassic);
175 let target_dir = temp_dir.path().join("hermetic");
176 fs::create_dir_all(&target_dir).unwrap();
177
178 let workspace_nm = temp_dir.path().join("node_modules");
180 fs::create_dir_all(&workspace_nm).unwrap();
181
182 let materializer = NodeModulesMaterializer;
183 let result = materializer.materialize(&workspace, &[], &target_dir);
184
185 assert!(result.is_ok());
186 assert!(target_dir.join("node_modules").exists());
187 }
188
189 #[test]
190 fn test_node_modules_materializer_handles_yarn_modern() {
191 let temp_dir = TempDir::new().unwrap();
192 let workspace = make_workspace(temp_dir.path(), PackageManager::YarnModern);
193 let target_dir = temp_dir.path().join("hermetic");
194 fs::create_dir_all(&target_dir).unwrap();
195
196 let workspace_nm = temp_dir.path().join("node_modules");
198 fs::create_dir_all(&workspace_nm).unwrap();
199
200 let materializer = NodeModulesMaterializer;
201 let result = materializer.materialize(&workspace, &[], &target_dir);
202
203 assert!(result.is_ok());
204 assert!(target_dir.join("node_modules").exists());
205 }
206
207 #[test]
208 fn test_node_modules_materializer_no_source_node_modules() {
209 let temp_dir = TempDir::new().unwrap();
210 let workspace = make_workspace(temp_dir.path(), PackageManager::Npm);
211 let target_dir = temp_dir.path().join("hermetic");
212 fs::create_dir_all(&target_dir).unwrap();
213
214 let materializer = NodeModulesMaterializer;
217 let result = materializer.materialize(&workspace, &[], &target_dir);
218
219 assert!(result.is_ok());
221 assert!(!target_dir.join("node_modules").exists());
222 }
223
224 #[test]
225 fn test_node_modules_materializer_skips_if_target_exists() {
226 let temp_dir = TempDir::new().unwrap();
227 let workspace = make_workspace(temp_dir.path(), PackageManager::Npm);
228 let target_dir = temp_dir.path().join("hermetic");
229 fs::create_dir_all(&target_dir).unwrap();
230
231 let workspace_nm = temp_dir.path().join("node_modules");
233 fs::create_dir_all(&workspace_nm).unwrap();
234 fs::write(workspace_nm.join("marker"), "source").unwrap();
235
236 let target_nm = target_dir.join("node_modules");
238 fs::create_dir_all(&target_nm).unwrap();
239 fs::write(target_nm.join("existing"), "keep").unwrap();
240
241 let materializer = NodeModulesMaterializer;
242 let result = materializer.materialize(&workspace, &[], &target_dir);
243
244 assert!(result.is_ok());
245 assert!(target_nm.join("existing").exists());
247 }
248
249 #[test]
254 fn test_detect_cache_dir_npm() {
255 unsafe { std::env::set_var("HOME", "/home/test") };
258 let cache = NodeModulesMaterializer::detect_cache_dir(PackageManager::Npm);
259 assert!(cache.is_some());
260 assert!(cache.unwrap().ends_with(".npm"));
261 }
262
263 #[test]
264 fn test_detect_cache_dir_pnpm() {
265 unsafe { std::env::set_var("HOME", "/home/test") };
267 let cache = NodeModulesMaterializer::detect_cache_dir(PackageManager::Pnpm);
268 assert!(cache.is_some());
269 let cache_path = cache.unwrap();
270 assert!(cache_path.to_string_lossy().contains("pnpm"));
271 }
272
273 #[test]
274 fn test_detect_cache_dir_bun() {
275 unsafe { std::env::set_var("HOME", "/home/test") };
277 let cache = NodeModulesMaterializer::detect_cache_dir(PackageManager::Bun);
278 assert!(cache.is_some());
279 let cache_path = cache.unwrap();
280 assert!(cache_path.to_string_lossy().contains("bun"));
281 }
282
283 #[test]
284 fn test_detect_cache_dir_yarn_classic() {
285 unsafe { std::env::set_var("HOME", "/home/test") };
287 let cache = NodeModulesMaterializer::detect_cache_dir(PackageManager::YarnClassic);
288 assert!(cache.is_some());
289 let cache_path = cache.unwrap();
290 assert!(cache_path.to_string_lossy().contains("yarn"));
291 }
292
293 #[test]
294 fn test_detect_cache_dir_yarn_modern() {
295 unsafe { std::env::set_var("HOME", "/home/test") };
297 let cache = NodeModulesMaterializer::detect_cache_dir(PackageManager::YarnModern);
298 assert!(cache.is_some());
299 let cache_path = cache.unwrap();
300 assert!(cache_path.to_string_lossy().contains("berry"));
301 }
302
303 #[test]
304 fn test_detect_cache_dir_deno() {
305 unsafe { std::env::set_var("HOME", "/home/test") };
307 let cache = NodeModulesMaterializer::detect_cache_dir(PackageManager::Deno);
308 assert!(cache.is_some());
309 let cache_path = cache.unwrap();
310 assert!(cache_path.to_string_lossy().contains("deno"));
311 }
312
313 #[test]
314 fn test_detect_cache_dir_cargo_returns_none() {
315 unsafe { std::env::set_var("HOME", "/home/test") };
317 let cache = NodeModulesMaterializer::detect_cache_dir(PackageManager::Cargo);
318 assert!(cache.is_none());
319 }
320}