cuenv_workspaces/materializer/
node_modules.rs

1//! Materializer for Node.js dependencies (`node_modules`).
2
3use 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
13/// Materializer for Node.js projects.
14pub 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        // Strategy:
35        // 1. Symlink the entire node_modules from workspace root if it exists.
36        //    This allows leveraging the existing installed dependencies.
37        // 2. (Future) Link individual packages from global cache (pnpm store, etc.)
38        //    for granular, hermetic population.
39
40        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            // Warn or log that dependencies might be missing?
53            // For now, we assume if node_modules is missing, the user might run install
54            // or we are in a state where it's not needed yet.
55        }
56
57        Ok(())
58    }
59}
60
61impl NodeModulesMaterializer {
62    /// Detects the global cache directory for the package manager.
63    ///
64    /// This can be used in the future to populate dependencies directly from cache.
65    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                // pnpm store path can vary (v3, etc), checking common default
74                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}