cuenv_workspaces/discovery/
package_json.rs1use crate::core::traits::WorkspaceDiscovery;
4use crate::core::types::{PackageManager, Workspace, WorkspaceMember};
5use crate::discovery::{read_json_file, resolve_glob_patterns};
6use crate::error::{Error, Result};
7use serde::Deserialize;
8use std::collections::HashMap;
9use std::path::Path;
10
11pub struct PackageJsonDiscovery;
17
18impl WorkspaceDiscovery for PackageJsonDiscovery {
19 fn discover(&self, root: &Path) -> Result<Workspace> {
20 let package_json_path = root.join("package.json");
21 if !package_json_path.exists() {
22 return Err(Error::WorkspaceNotFound {
23 path: root.to_path_buf(),
24 });
25 }
26
27 let package_json: PackageJson = read_json_file(&package_json_path)?;
28
29 if package_json.workspaces.is_none() {
32 let manager = detect_manager(root);
39 let mut workspace = Workspace::new(root.to_path_buf(), manager);
40 workspace.lockfile = find_lockfile(root, manager);
41 return Ok(workspace);
42 }
43
44 let members = self.find_members(root)?;
45 let manager = detect_manager(root);
46
47 let mut workspace = Workspace::new(root.to_path_buf(), manager);
48 workspace.members = members;
49 workspace.lockfile = find_lockfile(root, manager);
50
51 Ok(workspace)
52 }
53
54 fn find_members(&self, root: &Path) -> Result<Vec<WorkspaceMember>> {
55 let package_json_path = root.join("package.json");
56 let package_json: PackageJson = read_json_file(&package_json_path)?;
57
58 let patterns = match package_json.workspaces {
59 Some(WorkspacesField::Array(patterns)) => patterns,
60 Some(WorkspacesField::Object { packages, .. }) => packages,
61 None => return Ok(Vec::new()),
62 };
63
64 let matched_paths = resolve_glob_patterns(root, &patterns, &[])?;
65 let mut members = Vec::new();
66
67 for path in matched_paths {
68 if self.validate_member(&path)? {
69 let manifest_path = path.join("package.json");
70 let member_pkg: PackageJson = read_json_file(&manifest_path)?;
71
72 if let Some(name) = member_pkg.name {
73 let mut dependencies = Vec::new();
74 if let Some(deps) = member_pkg.dependencies {
75 dependencies.extend(deps.keys().cloned());
76 }
77 if let Some(dev_deps) = member_pkg.dev_dependencies {
78 dependencies.extend(dev_deps.keys().cloned());
79 }
80
81 members.push(WorkspaceMember {
82 name,
83 path: path.strip_prefix(root).unwrap_or(&path).to_path_buf(),
84 manifest_path,
85 dependencies,
86 });
87 }
88 }
89 }
90
91 members.sort_by(|a, b| a.name.cmp(&b.name));
92 Ok(members)
93 }
94
95 fn validate_member(&self, member_path: &Path) -> Result<bool> {
108 let manifest_path = member_path.join("package.json");
109 if !manifest_path.exists() {
110 return Ok(false);
111 }
112
113 match read_json_file::<PackageJson>(&manifest_path) {
115 Ok(pkg) => {
116 if pkg.name.is_some() {
117 Ok(true)
118 } else {
119 Ok(false)
120 }
121 }
122 Err(Error::Json { .. }) => Ok(false), Err(e) => Err(e), }
125 }
126}
127
128#[derive(Deserialize)]
129#[serde(rename_all = "camelCase")]
130struct PackageJson {
131 name: Option<String>,
132 workspaces: Option<WorkspacesField>,
133 dependencies: Option<HashMap<String, String>>,
134 dev_dependencies: Option<HashMap<String, String>>,
135}
136
137#[derive(Deserialize)]
138#[serde(untagged)]
139enum WorkspacesField {
140 Array(Vec<String>),
141 Object { packages: Vec<String> },
142}
143
144fn detect_manager(root: &Path) -> PackageManager {
145 if root.join("bun.lock").exists() {
146 PackageManager::Bun
147 } else if root.join("yarn.lock").exists() {
148 if root.join(".yarnrc.yml").exists() {
153 PackageManager::YarnModern
154 } else {
155 PackageManager::YarnClassic
156 }
157 } else if root.join("pnpm-lock.yaml").exists() {
158 PackageManager::Pnpm
159 } else {
160 PackageManager::Npm
161 }
162}
163
164fn find_lockfile(root: &Path, manager: PackageManager) -> Option<std::path::PathBuf> {
165 let lockfile = root.join(manager.lockfile_name());
166 if lockfile.exists() {
167 Some(lockfile)
168 } else {
169 None
170 }
171}