npm_pkg/
lib.rs

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
74/// Get npm package info
75///
76/// # Exmaple
77/// ```
78/// use std::{env::current_dir, vec};
79/// use npm_pkg::{get_package_info, is_package_exists, Options, PackageInfo, PackageJSON};
80
81/// let pkg_info = get_package_info("consola", Options::default());
82/// assert_eq!(pkg_info, Some(PackageInfo {
83/// name: String::from("consola"),
84/// version: String::from("3.2.3"),
85/// root_path: current_dir().unwrap().join("node_modules/consola"),
86/// package_json_path: current_dir().unwrap().join("node_modules/consola/package.json"),
87/// package_entry: current_dir().unwrap().join("node_modules/consola/dist/index.mjs"),
88/// package_json: PackageJSON {
89///     name: Some(String::from("consola")),
90///     version: Some(String::from("3.2.3")),
91///     description: Some(String::from("Elegant Console Wrapper")),
92///     homepage: None,
93///     keywords: Some(vec![String::from("console"), String::from("logger"), String::from("reporter"), String::from("elegant"), String::from("cli"), String::from("universal"), String::from("unified"), String::from("prompt"), String::from("clack"), String::from("format"), String::from("error"), String::from("stacktrace")]),
94///     license: Some(String::from("MIT")),
95///     private: None,
96///     author: None,
97///     files: Some(vec![String::from("dist"), String::from("lib"), String::from("*.d.ts")]),
98///     r#type: Some(String::from("module")),
99///     main: Some(String::from("./lib/index.cjs")),
100///     module: Some(String::from("./dist/index.mjs")),
101///     exports: None,
102///     types: Some(String::from("./dist/index.d.ts")),
103///     browser: Some(String::from("./dist/browser.mjs")),
104///     bin: None,
105///     scripts: Some(serde_json::from_str(r#"{ "build": "unbuild", "lint:fix": "eslint . --fix && prettier -w src examples test", "lint": "eslint . && prettier -c src examples test", "test": "pnpm lint && pnpm vitest run --coverage", "release": "pnpm test && pnpm build && changelogen --release --push && npm publish", "dev": "vitest" }"#).unwrap()),
106///     dependencies: None,
107///     dev_dependencies: Some(serde_json::from_str(r#"{ "typescript": "^5.1.6", "changelogen": "^0.5.3", "defu": "^6.1.2", "sentencer": "^0.2.1", "eslint-config-unjs": "^0.2.1", "lodash": "^4.17.21", "@vitest/coverage-v8": "^0.32.2", "prettier": "^3.0.0", "unbuild": "^1.2.1", "is-unicode-supported": "^1.3.0", "jiti": "^1.18.2", "@types/node": "^20.3.3", "eslint": "^8.44.0", "@clack/core": "^0.3.2", "vitest": "^0.32.2", "sisteransi": "^1.0.5", "std-env": "^3.3.3", "string-width": "^6.1.0" }"#).unwrap()),
108///     peer_dependencies: None,
109///     peer_dependencies_meta: None,
110///    optional_dependencies: None,
111///     engines: Some(serde_json::from_str(r#"{"node": "^14.18.0 || >=16.10.0"}"#).unwrap())
112/// }
113/// }));
114/// ```
115pub 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
159/// Get npm package info
160///
161/// # Exmaple
162/// ```
163/// use std::{env::current_dir, vec};
164/// use npm_pkg::{get_package_info, is_package_exists, Options, PackageInfo, PackageJSON};
165///
166/// assert!(is_package_exists("magic-string", &Options::default()));
167/// assert!(!is_package_exists("abc", &Options::default()));
168/// ```
169pub 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}