1 use anyhow::{anyhow, bail, Context};
3 use log::{debug, info};
5 use serde_json::{Map, Value};
9 process::{Command, Stdio},
11 use tempfile::{tempdir, TempDir};
21 force_empty_cache: bool,
22 ) -> anyhow::Result<Vec<Package>> {
23 debug!("parsing lockfile with contents:\n{content}");
25 let mut packages = lock::packages(content)
26 .context("failed to extract packages from lockfile")?
29 let n = p.name.clone().unwrap();
31 Package::from_lock(p).with_context(|| format!("failed to parse data for {n}"))
33 .collect::<anyhow::Result<Vec<_>>>()?;
35 if packages.is_empty() && !force_empty_cache {
36 bail!("No cacheable dependencies were found. Please inspect the upstream `package-lock.json` file and ensure that remote dependencies have `resolved` URLs and `integrity` hashes. If the lockfile is missing this data, attempt to get upstream to fix it via a tool like <https://github.com/jeslie0/npm-lockfile-fix>. If generating an empty cache is intentional and you would like to do it anyways, set `forceEmptyCache = true`.");
39 let mut new = Vec::new();
43 .filter(|p| matches!(p.specifics, Specifics::Git { .. }))
45 let dir = match &pkg.specifics {
46 Specifics::Git { workdir } => workdir,
47 Specifics::Registry { .. } => unimplemented!(),
50 let path = dir.path().join("package");
52 info!("recursively parsing lockfile for {} at {path:?}", pkg.name);
54 let lockfile_contents = fs::read_to_string(path.join("package-lock.json"));
56 let package_json_path = path.join("package.json");
57 let mut package_json: Map<String, Value> =
58 serde_json::from_str(&fs::read_to_string(package_json_path)?)?;
60 if let Some(scripts) = package_json
62 .and_then(Value::as_object_mut)
64 // https://github.com/npm/pacote/blob/272edc1bac06991fc5f95d06342334bbacfbaa4b/lib/git.js#L166-L172
73 if scripts.contains_key(typ) && lockfile_contents.is_err() && !force_git_deps {
74 bail!("Git dependency {} contains install scripts, but has no lockfile, which is something that will probably break. Open an issue if you can't feasibly patch this dependency out, and we'll come up with a workaround.\nIf you'd like to attempt to try to use this dependency anyways, set `forceGitDeps = true`.", pkg.name);
79 if let Ok(lockfile_contents) = lockfile_contents {
80 new.append(&mut lockfile(
83 // force_empty_cache is turned on here since recursively parsed lockfiles should be
84 // allowed to have an empty cache without erroring by default
90 packages.append(&mut new);
92 packages.par_sort_by(|x, y| {
95 .expect("resolved should be comparable")
98 packages.dedup_by(|x, y| x.url == y.url);
107 specifics: Specifics,
112 Registry { integrity: lock::Hash },
113 Git { workdir: TempDir },
117 fn from_lock(pkg: lock::Package) -> anyhow::Result<Package> {
118 let mut resolved = match pkg
120 .expect("at this point, packages should have URLs")
122 UrlOrString::Url(u) => u,
123 UrlOrString::String(_) => panic!("at this point, all packages should have URLs"),
126 let specifics = match get_hosted_git_url(&resolved)? {
128 let body = util::get_url_body_with_retry(&hosted)?;
130 let workdir = tempdir()?;
132 let tar_path = workdir.path().join("package");
134 fs::create_dir(&tar_path)?;
136 let mut cmd = Command::new("tar")
137 .args(["--extract", "--gzip", "--strip-components=1", "-C"])
139 .stdin(Stdio::piped())
142 cmd.stdin.take().unwrap().write_all(&body)?;
144 let exit = cmd.wait()?;
148 "failed to extract tarball for {}: tar exited with status code {}",
156 Specifics::Git { workdir }
158 None => Specifics::Registry {
161 .expect("non-git dependencies should have associated integrity")
163 .expect("non-git dependencies should have non-empty associated integrity"),
168 name: pkg.name.unwrap(),
174 pub fn tarball(&self) -> anyhow::Result<Vec<u8>> {
175 match &self.specifics {
176 Specifics::Registry { .. } => Ok(util::get_url_body_with_retry(&self.url)?),
177 Specifics::Git { workdir } => Ok(Command::new("tar")
197 pub fn integrity(&self) -> Option<&lock::Hash> {
198 match &self.specifics {
199 Specifics::Registry { integrity } => Some(integrity),
200 Specifics::Git { .. } => None,
205 #[allow(clippy::case_sensitive_file_extension_comparisons)]
206 fn get_hosted_git_url(url: &Url) -> anyhow::Result<Option<Url>> {
207 if ["git", "git+ssh", "git+https", "ssh"].contains(&url.scheme()) {
210 .ok_or_else(|| anyhow!("bad URL: {url}"))?;
212 let mut get_url = || match url.host_str()? {
214 let user = s.next()?;
215 let mut project = s.next()?;
217 let mut commit = s.next();
220 commit = url.fragment();
221 } else if typ.is_some() && typ != Some("tree") {
225 if project.ends_with(".git") {
226 project = project.strip_suffix(".git")?;
229 let commit = commit.unwrap();
233 "https://codeload.github.com/{user}/{project}/tar.gz/{commit}"
239 let user = s.next()?;
240 let mut project = s.next()?;
243 if aux == Some("get") {
247 if project.ends_with(".git") {
248 project = project.strip_suffix(".git")?;
251 let commit = url.fragment()?;
255 "https://bitbucket.org/{user}/{project}/get/{commit}.tar.gz"
261 /* let path = &url.path()[1..];
263 if path.contains("/~/") || path.contains("/archive.tar.gz") {
267 let user = s.next()?;
268 let mut project = s.next()?;
270 if project.ends_with(".git") {
271 project = project.strip_suffix(".git")?;
274 let commit = url.fragment()?;
278 "https://gitlab.com/{user}/{project}/repository/archive.tar.gz?ref={commit}"
283 // lmao: https://github.com/npm/hosted-git-info/pull/109
287 let user = s.next()?;
288 let mut project = s.next()?;
291 if aux == Some("archive") {
295 if project.ends_with(".git") {
296 project = project.strip_suffix(".git")?;
299 let commit = url.fragment()?;
303 "https://git.sr.ht/{user}/{project}/archive/{commit}.tar.gz"
312 Some(u) => Ok(Some(u)),
313 None => Err(anyhow!("This lockfile either contains a Git dependency with an unsupported host, or a malformed URL in the lockfile: {url}"))
322 use super::get_hosted_git_url;
326 fn hosted_git_urls() {
327 for (input, expected) in [
329 "git+ssh://git@github.com/castlabs/electron-releases.git#fc5f78d046e8d7cdeb66345a2633c383ab41f525",
330 Some("https://codeload.github.com/castlabs/electron-releases/tar.gz/fc5f78d046e8d7cdeb66345a2633c383ab41f525"),
333 "git+ssh://bitbucket.org/foo/bar#branch",
334 Some("https://bitbucket.org/foo/bar/get/branch.tar.gz")
337 "git+ssh://git.sr.ht/~foo/bar#branch",
338 Some("https://git.sr.ht/~foo/bar/archive/branch.tar.gz")
342 get_hosted_git_url(&Url::parse(input).unwrap()).unwrap(),
343 expected.map(|u| Url::parse(u).unwrap())
348 get_hosted_git_url(&Url::parse("ssh://git@gitlab.com/foo/bar.git#fix/bug").unwrap())
350 "GitLab URLs should be marked as invalid (lol)"