1use std::borrow::Cow;
5use std::collections::HashMap;
6use std::path::Path;
7use std::{fs, io};
8
9use chaste_types::{
10 Chastefile, ChastefileBuilder, Checksums, Dependency, DependencyBuilder, DependencyKind,
11 InstallationBuilder, Integrity, ModulePath, PackageBuilder, PackageID, PackageName,
12 PackageSource, SourceVersionSpecifier,
13};
14
15pub use crate::error::{Error, Result};
16
17use crate::types::{DependencyTreePackage, PackageLock, PeerDependencyMeta};
18
19mod error;
20#[cfg(test)]
21mod tests;
22mod types;
23
24pub static LOCKFILE_NAME: &str = "package-lock.json";
25pub static SHRINKWRAP_NAME: &str = "npm-shrinkwrap.json";
26
27struct PackageParser<'a> {
28 package_lock: &'a PackageLock<'a>,
29 chastefile_builder: ChastefileBuilder,
30 path_pid: HashMap<&'a Cow<'a, str>, PackageID>,
31}
32
33fn recognize_source(resolved: &str) -> Option<PackageSource> {
34 match resolved {
35 r if r.starts_with("https://registry.npmjs.org/") => Some(PackageSource::Npm),
39
40 r if r.starts_with("git+") => Some(PackageSource::Git { url: r.to_string() }),
41
42 _ => None,
43 }
44}
45
46fn parse_package(
47 path: &ModulePath,
48 tree_package: &DependencyTreePackage,
49) -> Result<PackageBuilder> {
50 let mut name = tree_package
51 .name
52 .as_ref()
53 .map(|s| PackageName::new(s.to_string()))
54 .transpose()?;
55 if name.is_none() {
58 name = path.implied_package_name();
59 }
60 let mut pkg = PackageBuilder::new(name, tree_package.version.as_ref().map(|s| s.to_string()));
61 if let Some(integrity) = &tree_package.integrity {
62 let inte: Integrity = integrity.parse()?;
63 if !inte.hashes.is_empty() {
64 pkg.checksums(Checksums::Tarball(inte));
65 }
66 }
67 if let Some(resolved) = &tree_package.resolved {
68 if let Some(source) = recognize_source(resolved) {
69 pkg.source(source);
70 }
71 }
72 Ok(pkg)
73}
74
75fn find_pid<'a>(
76 path: &str,
77 name: &str,
78 path_pid: &HashMap<&'a Cow<'a, str>, PackageID>,
79) -> Result<PackageID> {
80 let potential_path = match path {
81 "" => format!("node_modules/{name}"),
82 p => format!("{p}/node_modules/{name}"),
83 };
84 if let Some(pid) = path_pid.get(&Cow::Borrowed(potential_path.as_str())) {
85 return Ok(*pid);
86 }
87 if let Some((parent_path, _)) = path.rsplit_once('/') {
88 return find_pid(parent_path, name, path_pid);
89 }
90 if !path.is_empty() {
91 return find_pid("", name, path_pid);
92 }
93 Err(Error::DependencyNotFound(name.to_string()))
94}
95
96fn parse_dependencies<'a>(
97 path: &str,
98 tree_package: &'a DependencyTreePackage,
99 path_pid: &HashMap<&'a Cow<'a, str>, PackageID>,
100 self_pid: PackageID,
101) -> Result<Vec<Dependency>> {
102 let capacity = tree_package.dependencies.len() + tree_package.dev_dependencies.len();
103 let mut dependencies = Vec::with_capacity(capacity);
104 for (deps, kind_) in [
105 (&tree_package.dependencies, DependencyKind::Dependency),
106 (
107 &tree_package.dev_dependencies,
108 DependencyKind::DevDependency,
109 ),
110 (
111 &tree_package.peer_dependencies,
112 DependencyKind::PeerDependency,
113 ),
114 (
115 &tree_package.optional_dependencies,
116 DependencyKind::OptionalDependency,
117 ),
118 ] {
119 for (n, svs) in deps {
120 let kind = match kind_ {
121 DependencyKind::PeerDependency
122 if matches!(
123 tree_package.peer_dependencies_meta.get(n),
124 Some(PeerDependencyMeta {
125 optional: Some(true),
126 })
127 ) =>
128 {
129 DependencyKind::OptionalPeerDependency
130 }
131 k => k,
132 };
133 match find_pid(path, n, path_pid) {
134 Ok(pid) => {
135 let mut dep = DependencyBuilder::new(kind, self_pid, pid);
136 let svs = SourceVersionSpecifier::new(svs.to_string())?;
137 if svs.aliased_package_name().is_some() {
138 dep.alias_name(PackageName::new(n.to_string())?);
139 }
140 dep.svs(svs);
141 dependencies.push(dep.build());
142 }
143 Err(Error::DependencyNotFound(_)) if kind.is_peer() || kind.is_optional() => {}
147
148 Err(e) => return Err(e),
149 }
150 }
151 }
152
153 debug_assert!(dependencies.len() >= capacity);
154
155 Ok(dependencies)
156}
157
158impl<'a> PackageParser<'a> {
159 fn new(package_lock: &'a PackageLock) -> Self {
160 Self {
161 package_lock,
162 chastefile_builder: ChastefileBuilder::new(),
163 path_pid: HashMap::with_capacity(package_lock.packages.len()),
164 }
165 }
166
167 fn resolve(mut self) -> Result<Chastefile> {
168 for (package_path, tree_package) in self
171 .package_lock
172 .packages
173 .iter()
174 .filter(|(_, tp)| tp.link != Some(true))
175 {
176 let module_path = ModulePath::new(package_path.to_string())?;
177 let mut package = parse_package(&module_path, tree_package)?;
178 if package_path.is_empty() && package.get_name().is_none() {
179 package.name(Some(PackageName::new(self.package_lock.name.to_string())?));
180 }
181 let pid = match self.chastefile_builder.add_package(package.build()?) {
182 Ok(pid) => pid,
183 Err(chaste_types::Error::DuplicatePackage(pid)) => pid,
185 Err(e) => return Err(Error::ChasteError(e)),
186 };
187 self.path_pid.insert(package_path, pid);
188 let installation = InstallationBuilder::new(pid, module_path).build()?;
189 self.chastefile_builder
190 .add_package_installation(installation);
191 if package_path.is_empty() {
192 self.chastefile_builder.set_root_package_id(pid)?;
193
194 } else if !package_path.starts_with("node_modules/")
196 && !package_path.contains("/node_modules/")
197 {
198 self.chastefile_builder.set_as_workspace_member(pid)?;
199 }
200 }
201 for (package_path, tree_package) in self
203 .package_lock
204 .packages
205 .iter()
206 .filter(|(_, tp)| tp.link == Some(true))
207 {
208 let Some(member_path) = &tree_package.resolved else {
209 return Err(Error::WorkspaceMemberNotFound(package_path.to_string()));
210 };
211 let pid = *self.path_pid.get(member_path).unwrap();
212 self.path_pid.insert(package_path, pid);
213 let module_path = ModulePath::new(package_path.to_string())?;
214 let installation = InstallationBuilder::new(pid, module_path).build()?;
215 self.chastefile_builder
216 .add_package_installation(installation);
217 }
218 for (package_path, tree_package) in self
220 .package_lock
221 .packages
222 .iter()
223 .filter(|(_, tp)| tp.link != Some(true))
224 {
225 let pid = *self.path_pid.get(package_path).unwrap();
226 let dependencies = parse_dependencies(package_path, tree_package, &self.path_pid, pid)?;
227 self.chastefile_builder
228 .add_dependencies(dependencies.into_iter());
229 }
230 Ok(self.chastefile_builder.build()?)
231 }
232}
233
234fn parse_lock(package_lock: &PackageLock) -> Result<Chastefile> {
235 if ![2, 3].contains(&package_lock.lockfile_version) {
236 return Err(Error::UnknownLockVersion(package_lock.lockfile_version));
237 }
238 let parser = PackageParser::new(package_lock);
239 let chastefile = parser.resolve()?;
240 Ok(chastefile)
241}
242
243pub fn parse<P>(root_dir: P) -> Result<Chastefile>
244where
245 P: AsRef<Path>,
246{
247 let lockfile_contents = match fs::read_to_string(root_dir.as_ref().join(SHRINKWRAP_NAME)) {
248 Ok(c) => c,
249 Err(e) if e.kind() == io::ErrorKind::NotFound => {
250 fs::read_to_string(root_dir.as_ref().join(LOCKFILE_NAME))?
251 }
252 Err(e) => return Err(Error::IoError(e)),
253 };
254 let package_lock: PackageLock = serde_json::from_str(&lockfile_contents)?;
255 parse_lock(&package_lock)
256}