1use std::{
2 collections::HashMap,
3 env::current_dir,
4 fs,
5 path::{Path, PathBuf},
6 sync::LazyLock,
7};
8
9use serde::{Deserialize, Serialize};
10use validate_npm_package_name::validate;
11
12#[derive(Default)]
13pub struct Options<'a> {
14 pub cwd: Option<&'a str>,
15}
16
17#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
18pub enum BinType {
19 String(String),
20 HashMap(HashMap<String, String>),
21}
22
23#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
24pub enum ExportValue {
25 String(String),
26 HashMap(HashMap<String, String>),
27}
28
29#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
30pub struct PackageJSON {
31 pub name: Option<String>,
32 pub version: Option<String>,
33 pub description: Option<String>,
34 pub homepage: Option<String>,
35 pub keywords: Option<Vec<String>>,
36 pub license: Option<String>,
37 pub private: Option<bool>,
38 pub author: Option<String>,
39 pub files: Option<Vec<String>>,
40 pub r#type: Option<String>,
41 pub main: Option<String>,
42 pub module: Option<String>,
43 #[serde(flatten)]
44 pub exports: Option<HashMap<String, ExportValue>>,
45 pub types: Option<String>,
46 pub browser: Option<String>,
47 #[serde(flatten)]
48 pub bin: Option<BinType>,
49 pub scripts: Option<HashMap<String, String>>,
50 pub dependencies: Option<HashMap<String, String>>,
51 #[serde(rename = "devDependencies")]
52 pub dev_dependencies: Option<HashMap<String, String>>,
53 #[serde(rename = "peerDependencies")]
54 pub peer_dependencies: Option<HashMap<String, String>>,
55 #[serde(rename = "peerDependenciesMeta")]
56 pub peer_dependencies_meta: Option<HashMap<String, String>>,
57 #[serde(rename = "optionalDependencies")]
58 pub optional_dependencies: Option<HashMap<String, String>>,
59 pub engines: Option<HashMap<String, String>>,
60}
61
62#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)]
63pub struct PackageInfo {
64 pub name: String,
65 pub version: String,
66 pub root_path: PathBuf,
67 pub package_json_path: PathBuf,
68 pub package_entry: PathBuf,
69 pub package_json: PackageJSON,
70}
71
72static CURRENT_DIR: LazyLock<PathBuf> = LazyLock::new(|| current_dir().unwrap());
73
74pub fn get_package_info(name: &str, options: Options) -> Option<PackageInfo> {
116 let validate_result = validate(&name.to_string());
117
118 if !validate_result.valid_for_new_packages && !validate_result.valid_for_old_packages {
119 return None;
120 }
121
122 let package_json_path = get_package_json_path(name, &options);
123
124 package_json_path.as_ref()?;
125
126 let package_json = get_package_json(package_json_path.as_ref().unwrap().as_path());
127
128 let package_entry = get_package_entry(package_json_path.as_ref().unwrap().as_path());
129
130 if package_json.is_none() || package_entry.is_none() {
131 return None;
132 }
133
134 Some(PackageInfo {
135 name: name.to_string(),
136 version: package_json.clone().unwrap().version?,
137 root_path: package_json_path
138 .as_ref()
139 .unwrap()
140 .parent()
141 .unwrap()
142 .to_path_buf(),
143 package_entry: package_entry.unwrap(),
144 package_json_path: package_json_path.unwrap(),
145 package_json: package_json.unwrap(),
146 })
147}
148
149pub fn get_package_json_path(name: &str, options: &Options) -> Option<PathBuf> {
150 let id = format!("node_modules/{}/package.json", name);
151 let pkg_json_path = resolve(&id, options);
152
153 match pkg_json_path {
154 Ok(pkg_json_path) => Some(pkg_json_path),
155 Err(_) => None,
156 }
157}
158
159pub fn is_package_exists(name: &str, options: &Options) -> bool {
170 let pkg_json_path = get_package_json_path(name, options);
171
172 pkg_json_path.is_some()
173}
174
175fn get_package_json(path: &Path) -> Option<PackageJSON> {
176 let json = fs::read_to_string(path);
177
178 match json {
179 Ok(json) => Some(serde_json::from_str(&json).unwrap()),
180 Err(_) => None,
181 }
182}
183
184fn get_package_entry(path: &Path) -> Option<PathBuf> {
185 let pkg_json = get_package_json(path);
186
187 if let Some(pkg_json) = pkg_json {
188 let root = path.parent().unwrap();
189
190 if pkg_json.r#type.is_some_and(|t| t == "module") && pkg_json.module.is_some() {
191 Some(root.join(pkg_json.module.unwrap()))
192 } else if pkg_json
193 .exports
194 .as_ref()
195 .is_some_and(|exports| exports.contains_key("."))
196 {
197 let exports = pkg_json.exports.unwrap();
198 let root_entry = exports.get(".").unwrap();
199
200 match root_entry {
201 ExportValue::String(root_entry) => Some(root.join(root_entry)),
202 ExportValue::HashMap(root_entry) => {
203 if root_entry.get("import").is_some() {
204 Some(root.join(root_entry.get("import").unwrap()))
205 } else {
206 Some(root.join(root_entry.get("require").unwrap()))
207 }
208 }
209 }
210 } else {
211 Some(root.join(pkg_json.main.as_ref().unwrap()))
212 }
213 } else {
214 None
215 }
216}
217
218fn resolve(name: &str, options: &Options) -> Result<PathBuf, String> {
219 let cwd = match options.cwd {
220 Some(cwd) => Path::new(cwd),
221 None => CURRENT_DIR.as_path(),
222 };
223 let id = cwd.join(name);
224
225 if id.try_exists().unwrap() {
226 Ok(id)
227 } else {
228 Err(format!("Cannot find module {} from {:?}", name, id))
229 }
230}