1use std::collections::HashMap;
5use std::fs;
6use std::path::Path;
7
8use chaste_types::{
9 package_name_part, Chastefile, ChastefileBuilder, Checksums, DependencyBuilder, DependencyKind,
10 InstallationBuilder, Integrity, ModulePath, PackageBuilder, PackageName, PackageSource,
11 SourceVersionSpecifier, PACKAGE_JSON_FILENAME,
12};
13use nom::bytes::complete::tag;
14use nom::combinator::{opt, recognize, rest, verify};
15use nom::sequence::{preceded, terminated};
16use nom::{IResult, Parser};
17
18pub use crate::error::Error;
19use crate::error::Result;
20
21mod error;
22#[cfg(test)]
23mod tests;
24mod types;
25
26pub static LOCKFILE_NAME: &str = "pnpm-lock.yaml";
27
28fn package_name(input: &str) -> IResult<&str, &str, nom::error::Error<&str>> {
29 recognize((
30 opt(preceded(tag("@"), terminated(package_name_part, tag("/")))),
31 verify(package_name_part, |part: &str| {
32 part != "node_modules" && part != "favicon.ico"
33 }),
34 ))
35 .parse(input)
36}
37
38pub fn parse<P>(root_dir: P) -> Result<Chastefile>
39where
40 P: AsRef<Path>,
41{
42 let root_dir = root_dir.as_ref();
43
44 let lockfile_contents = fs::read_to_string(root_dir.join(LOCKFILE_NAME))?;
45 let lockfile: types::Lockfile = serde_norway::from_str(&lockfile_contents)?;
46
47 if lockfile.lockfile_version != "9.0" {
48 return Err(Error::UnknownLockfileVersion(
49 lockfile.lockfile_version.to_string(),
50 ));
51 }
52
53 let mut chastefile = ChastefileBuilder::new();
54
55 let mut importer_to_pid = HashMap::with_capacity(lockfile.importers.len());
56 for importer_path in lockfile.importers.keys() {
57 let package_json_contents = fs::read_to_string(if *importer_path == "." {
58 root_dir.join(PACKAGE_JSON_FILENAME)
59 } else {
60 root_dir.join(importer_path).join(PACKAGE_JSON_FILENAME)
61 })?;
62 let package_json: types::PackageJson = serde_json::from_str(&package_json_contents)?;
63 let importer_pkg = PackageBuilder::new(
64 package_json
65 .name
66 .map(|n| PackageName::new(n.to_string()))
67 .transpose()?,
68 package_json.version.map(|v| v.to_string()),
69 );
70 let importer_pid = chastefile.add_package(importer_pkg.build()?)?;
71 if *importer_path == "." {
72 chastefile.set_root_package_id(importer_pid)?;
73 } else {
74 chastefile.set_as_workspace_member(importer_pid)?;
75 }
76 importer_to_pid.insert(importer_path, importer_pid);
77 let installation = InstallationBuilder::new(
78 importer_pid,
79 ModulePath::new(if *importer_path == "." {
80 "".to_string()
81 } else {
82 importer_path.to_string()
83 })?,
84 )
85 .build()?;
86 chastefile.add_package_installation(installation);
87 }
88
89 let mut desc_pid = HashMap::with_capacity(lockfile.packages.len());
90 for (pkg_desc, pkg) in &lockfile.packages {
91 let (_, (package_name, _, package_svd)) =
92 (package_name, tag("@"), rest)
93 .parse(&pkg_desc)
94 .map_err(|_| Error::InvalidPackageDescriptor(pkg_desc.to_string()))?;
95 let version = pkg
96 .version
97 .as_deref()
98 .or(Some(package_svd))
99 .map(|v| v.to_string());
100 let mut package =
101 PackageBuilder::new(Some(PackageName::new(package_name.to_string())?), version);
102 if let Some(integrity) = pkg.resolution.integrity {
103 let inte: Integrity = integrity.parse()?;
104 if !inte.hashes.is_empty() {
105 package.checksums(Checksums::Tarball(inte));
106 }
107 }
108 if let Some(tarball_url) = &pkg.resolution.tarball {
109 if pkg.resolution.integrity.is_some() {
111 package.source(PackageSource::Npm);
112 } else {
113 package.source(PackageSource::TarballURL {
114 url: tarball_url.to_string(),
115 });
116 }
117 } else if let Some(git_url) = package_svd.strip_prefix("git+") {
118 package.source(PackageSource::Git {
119 url: git_url.to_string(),
120 });
121 } else if SourceVersionSpecifier::new(package_svd.to_string()).is_ok_and(|svs| svs.is_npm())
122 {
123 package.source(PackageSource::Npm);
124 }
125 let pkg_pid = chastefile.add_package(package.build()?)?;
126 desc_pid.insert(
127 (package_name, package_svd),
128 (pkg_pid, &pkg.peer_dependencies, &pkg.peer_dependencies_meta),
129 );
130 }
131
132 for (importer_path, importer) in &lockfile.importers {
133 let importer_pid = *importer_to_pid.get(importer_path).unwrap();
134 for (dependencies, kind) in [
135 (&importer.dependencies, DependencyKind::Dependency),
136 (&importer.dev_dependencies, DependencyKind::DevDependency),
137 (&importer.peer_dependencies, DependencyKind::PeerDependency),
138 (
139 &importer.optional_dependencies,
140 DependencyKind::OptionalDependency,
141 ),
142 ] {
143 for (dep_name, d) in dependencies {
144 if d.version.starts_with("link:") {
145 continue;
147 }
148 let mut is_aliased = false;
149 let dep_pid = {
150 if let Some((dep_pid, _, _)) = desc_pid.get(&(&dep_name, &d.version)) {
151 *dep_pid
152 } else if let Ok((aliased_dep_svd, aliased_dep_name)) =
153 terminated(package_name, tag("@")).parse(&d.version)
154 {
155 if let Some((dep_pid, _, _)) =
156 desc_pid.get(&(aliased_dep_name, aliased_dep_svd))
157 {
158 is_aliased = true;
159 *dep_pid
160 } else {
161 return Err(Error::DependencyPackageNotFound(d.version.to_string()));
162 }
163 } else {
164 return Err(Error::DependencyPackageNotFound(format!(
165 "{dep_name}@{}",
166 d.version
167 )));
168 }
169 };
170 let mut dep = DependencyBuilder::new(kind, importer_pid, dep_pid);
171 if is_aliased {
172 dep.alias_name(PackageName::new(dep_name.to_string())?);
173 }
174 dep.svs(SourceVersionSpecifier::new(d.specifier.to_string())?);
175 chastefile.add_dependency(dep.build());
176 }
177 }
178 }
179
180 for (pkg_desc, snap) in lockfile.snapshots {
181 let Some((pkg_svd, pkg_name)) = terminated(package_name, tag("@")).parse(&pkg_desc).ok()
182 else {
183 unreachable!();
184 };
185 let pkg_svd = pkg_svd.split_once("(").map(|(s, _)| s).unwrap_or(pkg_svd);
188 let (pkg_pid, _pkg_peers, _pkg_peers_meta) = *desc_pid
189 .get(&(pkg_name, pkg_svd))
190 .ok_or_else(|| Error::SnapshotNotFound(pkg_desc.to_string()))?;
191 for (dependencies, kind) in [
192 (&snap.dependencies, DependencyKind::Dependency),
193 (&snap.dev_dependencies, DependencyKind::DevDependency),
194 (
195 &snap.optional_dependencies,
196 DependencyKind::OptionalDependency,
197 ),
198 ] {
199 for (dep_name, dep_svd) in dependencies {
200 let dep_svd = dep_svd.split_once("(").map(|(s, _)| s).unwrap_or(&dep_svd);
201 let dep = DependencyBuilder::new(kind, pkg_pid, {
202 if let Some((dep_pid, _, _)) = desc_pid.get(&(&dep_name, dep_svd)) {
203 *dep_pid
204 } else if let Ok((aliased_dep_svd, aliased_dep_name)) =
205 terminated(package_name, tag("@")).parse(dep_svd)
206 {
207 if let Some((dep_pid, _, _)) =
208 desc_pid.get(&(aliased_dep_name, aliased_dep_svd))
209 {
210 *dep_pid
211 } else {
212 return Err(Error::DependencyPackageNotFound(dep_svd.to_string()));
213 }
214 } else {
215 return Err(Error::DependencyPackageNotFound(format!(
216 "{dep_name}@{dep_svd}"
217 )));
218 }
219 });
220 chastefile.add_dependency(dep.build());
221 }
222 }
223 }
224
225 Ok(chastefile.build()?)
226}