Skip to main content

ncu/
detector.rs

1use anyhow::Result;
2use std::path::PathBuf;
3
4/// Detected package.json file
5#[derive(Debug, Clone)]
6pub struct DetectedFile {
7    pub path: PathBuf,
8}
9
10/// Detects package.json files in a project, including workspace members
11pub struct ProjectDetector {
12    project_path: PathBuf,
13}
14
15impl ProjectDetector {
16    pub fn new(project_path: PathBuf) -> Self {
17        Self { project_path }
18    }
19
20    /// Detect all package.json files in the project
21    pub fn detect(&self) -> Result<Vec<DetectedFile>> {
22        let mut detected = Vec::new();
23
24        let package_json = self.project_path.join("package.json");
25        if !package_json.exists() {
26            return Ok(detected);
27        }
28
29        detected.push(DetectedFile {
30            path: package_json.clone(),
31        });
32
33        // Check for workspace packages (npm/yarn/pnpm workspaces)
34        if let Ok(content) = std::fs::read_to_string(&package_json)
35            && let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&content)
36                && let Some(workspaces) = self.get_workspaces(&parsed) {
37                    for pattern in workspaces {
38                        let member_jsons = self.expand_workspace_pattern(&pattern)?;
39                        for path in member_jsons {
40                            if path != package_json {
41                                detected.push(DetectedFile { path });
42                            }
43                        }
44                    }
45                }
46
47        Ok(detected)
48    }
49
50    /// Extract workspace patterns from package.json
51    fn get_workspaces(&self, parsed: &serde_json::Value) -> Option<Vec<String>> {
52        // npm/yarn format: "workspaces": ["packages/*"]
53        if let Some(workspaces) = parsed.get("workspaces") {
54            // Direct array format
55            if let Some(arr) = workspaces.as_array() {
56                return Some(
57                    arr.iter()
58                        .filter_map(|v| v.as_str().map(String::from))
59                        .collect(),
60                );
61            }
62            // Yarn format: { "packages": ["packages/*"] }
63            if let Some(packages) = workspaces.get("packages").and_then(|v| v.as_array()) {
64                return Some(
65                    packages
66                        .iter()
67                        .filter_map(|v| v.as_str().map(String::from))
68                        .collect(),
69                );
70            }
71        }
72        None
73    }
74
75    /// Expand a workspace pattern (may contain globs)
76    fn expand_workspace_pattern(&self, pattern: &str) -> Result<Vec<PathBuf>> {
77        let mut results = Vec::new();
78        let full_pattern = self.project_path.join(pattern).join("package.json");
79        let pattern_str = full_pattern.to_string_lossy();
80
81        if let Ok(paths) = glob::glob(&pattern_str) {
82            for entry in paths.flatten() {
83                if entry.exists() {
84                    results.push(entry);
85                }
86            }
87        }
88
89        Ok(results)
90    }
91
92    /// Check if a lock file exists and return which type
93    pub fn detect_lockfile(&self) -> Option<LockfileType> {
94        if self.project_path.join("package-lock.json").exists() {
95            Some(LockfileType::Npm)
96        } else if self.project_path.join("pnpm-lock.yaml").exists() {
97            Some(LockfileType::Pnpm)
98        } else if self.project_path.join("yarn.lock").exists() {
99            Some(LockfileType::Yarn)
100        } else if self.project_path.join("bun.lockb").exists() {
101            Some(LockfileType::Bun)
102        } else {
103            None
104        }
105    }
106
107    pub fn lockfile_path(&self, lockfile_type: LockfileType) -> PathBuf {
108        match lockfile_type {
109            LockfileType::Npm => self.project_path.join("package-lock.json"),
110            LockfileType::Pnpm => self.project_path.join("pnpm-lock.yaml"),
111            LockfileType::Yarn => self.project_path.join("yarn.lock"),
112            LockfileType::Bun => self.project_path.join("bun.lockb"),
113        }
114    }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum LockfileType {
119    Npm,
120    Pnpm,
121    Yarn,
122    Bun,
123}