deno_lockfile/
lib.rs

1// Copyright 2018-2024 the Deno authors. MIT license.
2
3#![deny(clippy::print_stderr)]
4#![deny(clippy::print_stdout)]
5
6mod error;
7mod graphs;
8
9use std::borrow::Cow;
10use std::collections::btree_map::Entry as BTreeMapEntry;
11use std::collections::hash_map::Entry as HashMapEntry;
12use std::collections::BTreeMap;
13use std::collections::HashMap;
14use std::collections::HashSet;
15use std::path::PathBuf;
16
17use deno_semver::jsr::JsrDepPackageReq;
18use deno_semver::package::PackageNv;
19use deno_semver::SmallStackString;
20use deno_semver::StackString;
21use serde::de::DeserializeOwned;
22use serde::Deserialize;
23use serde::Serialize;
24
25mod printer;
26mod transforms;
27
28pub use error::DeserializationError;
29pub use error::LockfileError;
30pub use error::LockfileErrorReason;
31pub use transforms::Lockfile5NpmInfo;
32pub use transforms::NpmPackageInfoProvider;
33
34use crate::graphs::LockfilePackageGraph;
35
36pub struct SetWorkspaceConfigOptions {
37  pub config: WorkspaceConfig,
38  /// Maintains deno.json dependencies and workspace config
39  /// regardless of the `config` options provided.
40  ///
41  /// Ex. the CLI sets this to `true` when someone runs a
42  /// one-off script with `--no-config`.
43  pub no_config: bool,
44  /// Maintains package.json dependencies regardless of the
45  /// `config` options provided.
46  ///
47  /// Ex. the CLI sets this to `true` when someone runs a
48  /// one-off script with `--no-npm`.
49  pub no_npm: bool,
50}
51
52#[derive(Default, Debug, Clone, PartialEq, Eq)]
53pub struct WorkspaceConfig {
54  pub root: WorkspaceMemberConfig,
55  pub members: HashMap<String, WorkspaceMemberConfig>,
56  pub patches: HashMap<String, LockfilePatchContent>,
57}
58
59#[derive(Default, Debug, Clone, PartialEq, Eq)]
60pub struct WorkspaceMemberConfig {
61  pub dependencies: HashSet<JsrDepPackageReq>,
62  pub package_json_deps: HashSet<JsrDepPackageReq>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct NpmPackageLockfileInfo {
67  pub serialized_id: StackString,
68  /// Will be `None` for patch packages.
69  pub integrity: Option<String>,
70  pub dependencies: Vec<NpmPackageDependencyLockfileInfo>,
71  pub optional_dependencies: Vec<NpmPackageDependencyLockfileInfo>,
72  pub optional_peers: Vec<NpmPackageDependencyLockfileInfo>,
73  pub os: Vec<SmallStackString>,
74  pub cpu: Vec<SmallStackString>,
75  pub tarball: Option<StackString>,
76  pub deprecated: bool,
77  pub scripts: bool,
78  pub bin: bool,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct NpmPackageDependencyLockfileInfo {
83  pub name: StackString,
84  pub id: StackString,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)]
88pub struct NpmPackageInfo {
89  /// Will be `None` for patch packages.
90  pub integrity: Option<String>,
91  #[serde(default)]
92  pub dependencies: BTreeMap<StackString, StackString>,
93  #[serde(default)]
94  pub optional_dependencies: BTreeMap<StackString, StackString>,
95  #[serde(default)]
96  pub optional_peers: BTreeMap<StackString, StackString>,
97  #[serde(default, skip_serializing_if = "Vec::is_empty")]
98  pub os: Vec<SmallStackString>,
99  #[serde(default, skip_serializing_if = "Vec::is_empty")]
100  pub cpu: Vec<SmallStackString>,
101  #[serde(skip_serializing_if = "Option::is_none")]
102  pub tarball: Option<StackString>,
103  #[serde(default, skip_serializing_if = "is_false")]
104  pub deprecated: bool,
105  #[serde(default, skip_serializing_if = "is_false")]
106  pub scripts: bool,
107  #[serde(default, skip_serializing_if = "is_false")]
108  pub bin: bool,
109}
110
111fn is_false(value: &bool) -> bool {
112  !value
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)]
116pub struct NpmPackageDist {
117  pub shasum: String,
118  pub integrity: Option<String>,
119}
120
121#[derive(Debug, Clone)]
122pub struct JsrPackageInfo {
123  pub integrity: String,
124  /// List of package requirements found in the dependency.
125  ///
126  /// This is used to tell when a package can be removed from the lockfile.
127  pub dependencies: HashSet<JsrDepPackageReq>,
128}
129
130#[derive(Clone, Debug, Default)]
131pub struct PackagesContent {
132  /// Mapping between requests for jsr specifiers and resolved packages, eg.
133  /// {
134  ///   "jsr:@foo/bar@^2.1": "2.1.3",
135  ///   "npm:@ts-morph/common@^11": "11.0.0",
136  ///   "npm:@ts-morph/common@^12": "12.0.0__some-peer-dep@1.0.0",
137  /// }
138  pub specifiers: HashMap<JsrDepPackageReq, SmallStackString>,
139
140  /// Mapping between resolved jsr specifiers and their associated info, eg.
141  /// {
142  ///   "@oak/oak@12.6.3": {
143  ///     "dependencies": [
144  ///       "jsr:@std/bytes@0.210",
145  ///       // ...etc...
146  ///       "npm:path-to-regexpr@^6.2"
147  ///     ]
148  ///   }
149  /// }
150  pub jsr: BTreeMap<PackageNv, JsrPackageInfo>,
151
152  /// Mapping between resolved npm specifiers and their associated info, eg.
153  /// {
154  ///   "chalk@5.0.0": {
155  ///     "integrity": "sha512-...",
156  ///     "dependencies": {
157  ///       "ansi-styles": "ansi-styles@4.1.0",
158  ///     }
159  ///   }
160  /// }
161  pub npm: BTreeMap<StackString, NpmPackageInfo>,
162}
163
164impl PackagesContent {
165  fn is_empty(&self) -> bool {
166    self.specifiers.is_empty() && self.npm.is_empty() && self.jsr.is_empty()
167  }
168}
169
170#[derive(Debug, Default, Clone, Deserialize)]
171pub(crate) struct LockfilePackageJsonContent {
172  pub dependencies: HashSet<JsrDepPackageReq>,
173}
174
175impl LockfilePackageJsonContent {
176  pub fn is_empty(&self) -> bool {
177    self.dependencies.is_empty()
178  }
179}
180
181#[derive(Debug, Default, Clone, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub(crate) struct WorkspaceMemberConfigContent {
184  #[serde(default)]
185  pub dependencies: HashSet<JsrDepPackageReq>,
186  #[serde(default)]
187  pub package_json: LockfilePackageJsonContent,
188}
189
190impl WorkspaceMemberConfigContent {
191  pub fn is_empty(&self) -> bool {
192    self.dependencies.is_empty() && self.package_json.is_empty()
193  }
194
195  pub fn dep_reqs(&self) -> impl Iterator<Item = &JsrDepPackageReq> {
196    self
197      .package_json
198      .dependencies
199      .iter()
200      .chain(self.dependencies.iter())
201  }
202}
203
204#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
205#[serde(rename_all = "camelCase")]
206pub struct LockfilePatchContent {
207  #[serde(default)]
208  #[serde(skip_serializing_if = "Vec::is_empty")]
209  pub dependencies: HashSet<JsrDepPackageReq>,
210  #[serde(default)]
211  #[serde(skip_serializing_if = "Vec::is_empty")]
212  pub peer_dependencies: HashSet<JsrDepPackageReq>,
213  #[serde(default)]
214  #[serde(skip_serializing_if = "HashMap::is_empty")]
215  pub peer_dependencies_meta: HashMap<String, serde_json::Value>,
216}
217
218#[derive(Debug, Default, Clone, Deserialize)]
219#[serde(rename_all = "camelCase")]
220pub(crate) struct WorkspaceConfigContent {
221  #[serde(default, flatten)]
222  pub root: WorkspaceMemberConfigContent,
223  #[serde(default)]
224  pub members: HashMap<String, WorkspaceMemberConfigContent>,
225  #[serde(default)]
226  pub patches: HashMap<String, LockfilePatchContent>,
227}
228
229impl WorkspaceConfigContent {
230  pub fn is_empty(&self) -> bool {
231    self.root.is_empty() && self.members.is_empty() && self.patches.is_empty()
232  }
233
234  fn get_all_dep_reqs(&self) -> impl Iterator<Item = &JsrDepPackageReq> {
235    self
236      .root
237      .dep_reqs()
238      .chain(self.members.values().flat_map(|m| m.dep_reqs()))
239  }
240}
241
242#[derive(Debug, Default, Clone)]
243pub struct LockfileContent {
244  pub packages: PackagesContent,
245  pub redirects: BTreeMap<String, String>,
246  /// Mapping between URLs and their checksums for "http:" and "https:" deps
247  pub(crate) remote: BTreeMap<String, String>,
248  pub(crate) workspace: WorkspaceConfigContent,
249}
250
251impl LockfileContent {
252  pub fn from_json(
253    json: serde_json::Value,
254  ) -> Result<Self, DeserializationError> {
255    fn extract_nv_from_id(value: &str) -> Option<(&str, &str)> {
256      if value.is_empty() {
257        return None;
258      }
259      let at_index = value[1..].find('@').map(|i| i + 1)?;
260      let name = &value[..at_index];
261      let version = &value[at_index + 1..];
262      Some((name, version))
263    }
264
265    fn handle_dep(
266      dep: StackString,
267      version_by_dep_name: &HashMap<StackString, StackString>,
268      dependencies: &mut BTreeMap<StackString, StackString>,
269    ) -> Result<(), DeserializationError> {
270      let (left, right) = match extract_nv_from_id(&dep) {
271        Some((name, version)) => (name, version),
272        None => match version_by_dep_name.get(&dep) {
273          Some(version) => (dep.as_str(), version.as_str()),
274          None => return Err(DeserializationError::MissingPackage(dep)),
275        },
276      };
277      let (key, package_name, version) = match right.strip_prefix("npm:") {
278        Some(right) => {
279          // ex. key@npm:package-a@version
280          match extract_nv_from_id(right) {
281            Some((package_name, version)) => (left, package_name, version),
282            None => {
283              return Err(DeserializationError::InvalidNpmPackageDependency(
284                dep,
285              ));
286            }
287          }
288        }
289        None => (left, left, right),
290      };
291      dependencies.insert(key.into(), {
292        let mut text =
293          StackString::with_capacity(package_name.len() + 1 + version.len());
294        text.push_str(package_name);
295        text.push('@');
296        text.push_str(version);
297        text
298      });
299      Ok(())
300    }
301
302    #[derive(Debug, Deserialize)]
303    #[serde(rename_all = "camelCase")]
304    struct RawNpmPackageInfo {
305      pub integrity: Option<String>,
306      #[serde(default)]
307      pub dependencies: Vec<StackString>,
308      #[serde(default)]
309      pub optional_dependencies: Vec<StackString>,
310      #[serde(default, skip_serializing_if = "Vec::is_empty")]
311      pub optional_peers: Vec<StackString>,
312      #[serde(default)]
313      pub os: Vec<SmallStackString>,
314      #[serde(default)]
315      pub cpu: Vec<SmallStackString>,
316      #[serde(skip_serializing_if = "Option::is_none")]
317      pub tarball: Option<StackString>,
318      #[serde(default, skip_serializing_if = "is_false")]
319      pub deprecated: bool,
320      #[serde(default, skip_serializing_if = "is_false")]
321      pub scripts: bool,
322      #[serde(default, skip_serializing_if = "is_false")]
323      pub bin: bool,
324    }
325
326    #[derive(Debug, Deserialize)]
327    struct RawJsrPackageInfo {
328      pub integrity: String,
329      #[serde(default)]
330      pub dependencies: Vec<StackString>,
331    }
332
333    fn deserialize_section<T: DeserializeOwned + Default>(
334      json: &mut serde_json::Map<String, serde_json::Value>,
335      key: &'static str,
336    ) -> Result<T, DeserializationError> {
337      match json.remove(key) {
338        Some(value) => serde_json::from_value(value)
339          .map_err(|err| DeserializationError::FailedDeserializing(key, err)),
340        None => Ok(Default::default()),
341      }
342    }
343
344    use serde_json::Value;
345
346    let Value::Object(mut json) = json else {
347      return Ok(Self::default());
348    };
349
350    Ok(LockfileContent {
351      packages: {
352        let deserialized_specifiers: BTreeMap<StackString, SmallStackString> =
353          deserialize_section(&mut json, "specifiers")?;
354        let mut specifiers =
355          HashMap::with_capacity(deserialized_specifiers.len());
356        for (key, value) in deserialized_specifiers {
357          let dep = JsrDepPackageReq::from_str_loose(&key)?;
358          specifiers.insert(dep, value);
359        }
360
361        let mut npm: BTreeMap<StackString, NpmPackageInfo> = Default::default();
362        let raw_npm: BTreeMap<StackString, RawNpmPackageInfo> =
363          deserialize_section(&mut json, "npm")?;
364        if !raw_npm.is_empty() {
365          // collect the versions
366          let mut version_by_dep_name: HashMap<StackString, StackString> =
367            HashMap::with_capacity(raw_npm.len());
368          for id in raw_npm.keys() {
369            let Some((name, version)) = extract_nv_from_id(id) else {
370              return Err(DeserializationError::InvalidNpmPackageId(
371                id.clone(),
372              ));
373            };
374            version_by_dep_name.insert(name.into(), version.into());
375          }
376
377          // now go through and create the resolved npm package information
378          for (key, value) in raw_npm {
379            let mut dependencies: BTreeMap<StackString, StackString> =
380              BTreeMap::new();
381            let mut optional_dependencies =
382              BTreeMap::<StackString, StackString>::new();
383            let mut optional_peers =
384              BTreeMap::<StackString, StackString>::new();
385
386            for dep in value.dependencies.into_iter() {
387              handle_dep(dep, &version_by_dep_name, &mut dependencies)?;
388            }
389            for dep in value.optional_dependencies.into_iter() {
390              handle_dep(
391                dep,
392                &version_by_dep_name,
393                &mut optional_dependencies,
394              )?;
395            }
396            for dep in value.optional_peers.into_iter() {
397              handle_dep(dep, &version_by_dep_name, &mut optional_peers)?;
398            }
399
400            npm.insert(
401              key,
402              NpmPackageInfo {
403                integrity: value.integrity,
404                dependencies,
405                cpu: value.cpu,
406                os: value.os,
407                tarball: value.tarball,
408                optional_dependencies,
409                optional_peers,
410                deprecated: value.deprecated,
411                scripts: value.scripts,
412                bin: value.bin,
413              },
414            );
415          }
416        }
417        let mut jsr: BTreeMap<PackageNv, JsrPackageInfo> = Default::default();
418        {
419          let raw_jsr: BTreeMap<PackageNv, RawJsrPackageInfo> =
420            deserialize_section(&mut json, "jsr")?;
421          if !raw_jsr.is_empty() {
422            // collect the specifier information
423            let mut to_resolved_specifiers: HashMap<
424              Cow<JsrDepPackageReq>,
425              &JsrDepPackageReq,
426            > = HashMap::with_capacity(specifiers.len() * 2);
427            // first insert the specifiers with the version reqs
428            for dep in specifiers.keys() {
429              to_resolved_specifiers.insert(Cow::Borrowed(dep), dep);
430            }
431            // then insert the specifiers without version reqs
432            for dep in specifiers.keys() {
433              let Ok(dep_no_version_req) = JsrDepPackageReq::from_str(
434                &format!("{}{}", dep.kind.scheme_with_colon(), dep.req.name),
435              ) else {
436                continue; // should never happen
437              };
438              let entry =
439                to_resolved_specifiers.entry(Cow::Owned(dep_no_version_req));
440              // if an entry is occupied that means there's multiple specifiers
441              // for the same name, such as one without a req, so ignore inserting
442              // here
443              if let HashMapEntry::Vacant(entry) = entry {
444                entry.insert(dep);
445              }
446            }
447
448            // now go through the dependencies mapping to the new ones
449            for (key, value) in raw_jsr {
450              let mut dependencies =
451                HashSet::with_capacity(value.dependencies.len());
452              for dep in value.dependencies {
453                let raw_dep = dep;
454                let Ok(dep) = JsrDepPackageReq::from_str(&raw_dep) else {
455                  continue; // should never happen
456                };
457                let Some(resolved_dep) = to_resolved_specifiers.get(&dep)
458                else {
459                  return Err(DeserializationError::InvalidJsrDependency {
460                    dependency: raw_dep,
461                    package: key,
462                  });
463                };
464                dependencies.insert((*resolved_dep).clone());
465              }
466              jsr.insert(
467                key,
468                JsrPackageInfo {
469                  integrity: value.integrity,
470                  dependencies,
471                },
472              );
473            }
474          }
475        }
476
477        PackagesContent {
478          specifiers,
479          jsr,
480          npm,
481        }
482      },
483      redirects: deserialize_section(&mut json, "redirects")?,
484      remote: deserialize_section(&mut json, "remote")?,
485      workspace: deserialize_section(&mut json, "workspace")?,
486    })
487  }
488
489  pub fn is_empty(&self) -> bool {
490    self.packages.is_empty()
491      && self.redirects.is_empty()
492      && self.remote.is_empty()
493      && self.workspace.is_empty()
494  }
495}
496
497pub struct NewLockfileOptions<'a> {
498  pub file_path: PathBuf,
499  pub content: &'a str,
500  pub overwrite: bool,
501}
502
503#[derive(Debug, Clone)]
504pub struct Lockfile {
505  pub overwrite: bool,
506  pub has_content_changed: bool,
507  pub content: LockfileContent,
508  pub filename: PathBuf,
509}
510
511impl Lockfile {
512  pub fn new_empty(filename: PathBuf, overwrite: bool) -> Lockfile {
513    Lockfile {
514      overwrite,
515      has_content_changed: false,
516      content: LockfileContent::default(),
517      filename,
518    }
519  }
520
521  pub async fn new(
522    opts: NewLockfileOptions<'_>,
523    provider: &dyn NpmPackageInfoProvider,
524  ) -> Result<Lockfile, Box<LockfileError>> {
525    async fn load_content(
526      content: &str,
527      provider: &dyn NpmPackageInfoProvider,
528    ) -> Result<LockfileContent, LockfileErrorReason> {
529      let value: serde_json::Map<String, serde_json::Value> =
530        serde_json::from_str(content)
531          .map_err(LockfileErrorReason::ParseError)?;
532      let version = value.get("version").and_then(|v| v.as_str());
533      // When the value is transformed, we don't consider that a lockfile
534      // change that should update the lockfile because we want to reduce
535      // lockfile churn. For example, say someone with a new version of
536      // Deno does a PR to a repo that has an old lockfile, but they
537      // don't update any dependencies. In that case, we don't want to
538      // have that PR include a lockfile change.
539      let value = match version {
540        Some("5") => value,
541        Some("4") => transforms::transform4_to_5(value, provider).await?,
542        Some("3") => {
543          transforms::transform4_to_5(
544            transforms::transform3_to_4(value)?,
545            provider,
546          )
547          .await?
548        }
549        Some("2") => {
550          transforms::transform4_to_5(
551            transforms::transform3_to_4(transforms::transform2_to_3(value))?,
552            provider,
553          )
554          .await?
555        }
556        None => {
557          transforms::transform4_to_5(
558            transforms::transform3_to_4(transforms::transform2_to_3(
559              transforms::transform1_to_2(value),
560            ))?,
561            provider,
562          )
563          .await?
564        }
565        Some(version) => {
566          return Err(LockfileErrorReason::UnsupportedVersion {
567            version: version.to_string(),
568          });
569        }
570      };
571      let content = LockfileContent::from_json(value.into())
572        .map_err(LockfileErrorReason::DeserializationError)?;
573
574      Ok(content)
575    }
576
577    // Writing a lock file always uses the new format.
578    if opts.overwrite {
579      return Ok(Lockfile {
580        overwrite: opts.overwrite,
581        filename: opts.file_path,
582        has_content_changed: false,
583        content: LockfileContent::default(),
584      });
585    }
586
587    if opts.content.trim().is_empty() {
588      return Err(Box::new(LockfileError {
589        file_path: opts.file_path.display().to_string(),
590        source: LockfileErrorReason::Empty,
591      }));
592    }
593    let content =
594      load_content(opts.content, provider)
595        .await
596        .map_err(|reason| LockfileError {
597          file_path: opts.file_path.display().to_string(),
598          source: reason,
599        })?;
600    Ok(Lockfile {
601      overwrite: opts.overwrite,
602      has_content_changed: false,
603      content,
604      filename: opts.file_path,
605    })
606  }
607
608  pub fn as_json_string(&self) -> String {
609    let mut text = printer::print_v5_content(&self.content);
610    text.reserve(1);
611    text.push('\n');
612    text
613  }
614
615  pub fn set_workspace_config(
616    &mut self,
617    mut options: SetWorkspaceConfigOptions,
618  ) {
619    fn update_workspace_member(
620      has_content_changed: &mut bool,
621      removed_deps: &mut HashSet<JsrDepPackageReq>,
622      current: &mut WorkspaceMemberConfigContent,
623      new: WorkspaceMemberConfig,
624    ) {
625      if new.dependencies != current.dependencies {
626        let old_deps =
627          std::mem::replace(&mut current.dependencies, new.dependencies);
628
629        removed_deps.extend(old_deps);
630
631        *has_content_changed = true;
632      }
633
634      if new.package_json_deps != current.package_json.dependencies {
635        // update self.content.package_json
636        let old_package_json_deps = std::mem::replace(
637          &mut current.package_json.dependencies,
638          new.package_json_deps,
639        );
640
641        removed_deps.extend(old_package_json_deps);
642
643        *has_content_changed = true;
644      }
645    }
646
647    // if specified, don't modify the package.json dependencies
648    if options.no_npm || options.no_config {
649      if options.config.root.package_json_deps.is_empty() {
650        options
651          .config
652          .root
653          .package_json_deps
654          .clone_from(&self.content.workspace.root.package_json.dependencies);
655      }
656      for (key, value) in options.config.members.iter_mut() {
657        if value.package_json_deps.is_empty() {
658          value.package_json_deps = self
659            .content
660            .workspace
661            .members
662            .get(key)
663            .map(|m| m.package_json.dependencies.clone())
664            .unwrap_or_default();
665        }
666      }
667    }
668    if options.no_config {
669      if options.config.root.dependencies.is_empty() {
670        options
671          .config
672          .root
673          .dependencies
674          .clone_from(&self.content.workspace.root.dependencies);
675      }
676      for (key, value) in options.config.members.iter_mut() {
677        if value.dependencies.is_empty() {
678          value.dependencies = self
679            .content
680            .workspace
681            .members
682            .get(key)
683            .map(|m| m.dependencies.clone())
684            .unwrap_or_default();
685        }
686      }
687      for (key, value) in self.content.workspace.members.iter() {
688        if !options.config.members.contains_key(key) {
689          options.config.members.insert(
690            key.clone(),
691            WorkspaceMemberConfig {
692              dependencies: value.dependencies.clone(),
693              package_json_deps: value.package_json.dependencies.clone(),
694            },
695          );
696        }
697      }
698    }
699
700    // If the lockfile is empty, it's most likely not created yet and so
701    // we don't want this information being added to the lockfile to cause
702    // a lockfile to be created. If this is the case, revert the lockfile back
703    // to !self.has_content_changed after populating it with this information
704    let allow_content_changed =
705      self.has_content_changed || !self.content.is_empty();
706
707    let has_any_patch_changed = options.config.patches.len()
708      != self.content.workspace.patches.len()
709      || !options.config.patches.is_empty()
710        && options.config.patches.iter().all(|(patch, new)| {
711          let Some(existing) = self.content.workspace.patches.get_mut(patch)
712          else {
713            return true;
714          };
715          new != existing
716        });
717
718    // if a patch changes, it's quite complicated to figure out how to get it to redo
719    // npm resolution just for that part, so for now, clear out all the npm dependencies
720    // if any patch changes
721    if has_any_patch_changed {
722      self.has_content_changed = true;
723      self.content.packages.npm.clear();
724      self
725        .content
726        .packages
727        .specifiers
728        .retain(|k, _| match k.kind {
729          deno_semver::package::PackageKind::Jsr => true,
730          deno_semver::package::PackageKind::Npm => false,
731        });
732      self.content.workspace.patches.clear();
733      self
734        .content
735        .workspace
736        .patches
737        .extend(options.config.patches);
738    }
739
740    let old_deps = self
741      .content
742      .workspace
743      .get_all_dep_reqs()
744      .cloned()
745      .collect::<HashSet<_>>();
746    let mut removed_deps = HashSet::new();
747
748    // set the root
749    update_workspace_member(
750      &mut self.has_content_changed,
751      &mut removed_deps,
752      &mut self.content.workspace.root,
753      options.config.root,
754    );
755
756    // now go through the workspaces
757    let mut unhandled_members = self
758      .content
759      .workspace
760      .members
761      .keys()
762      .cloned()
763      .collect::<HashSet<_>>();
764    for (member_name, new_member) in options.config.members {
765      unhandled_members.remove(&member_name);
766      let current_member = self
767        .content
768        .workspace
769        .members
770        .entry(member_name)
771        .or_default();
772      update_workspace_member(
773        &mut self.has_content_changed,
774        &mut removed_deps,
775        current_member,
776        new_member,
777      );
778    }
779
780    for member in unhandled_members {
781      if let Some(member) = self.content.workspace.members.remove(&member) {
782        removed_deps.extend(member.dep_reqs().cloned());
783        self.has_content_changed = true;
784      }
785    }
786
787    // update the removed deps to keep what's still found in the workspace
788    for dep in self.content.workspace.get_all_dep_reqs() {
789      removed_deps.remove(dep);
790    }
791
792    if !removed_deps.is_empty() {
793      let packages = std::mem::take(&mut self.content.packages);
794      let remotes = std::mem::take(&mut self.content.remote);
795
796      // create the graph
797      let mut graph = LockfilePackageGraph::from_lockfile(
798        packages,
799        remotes,
800        old_deps.into_iter(),
801      );
802
803      // remove the packages
804      graph.remove_root_packages(removed_deps.into_iter());
805
806      // now populate the graph back into the packages
807      graph.populate_packages(
808        &mut self.content.packages,
809        &mut self.content.remote,
810      );
811    }
812
813    if !allow_content_changed {
814      // revert it back so this change doesn't by itself cause
815      // a lockfile to be created.
816      self.has_content_changed = false;
817    }
818  }
819
820  /// Gets the bytes that should be written to the disk.
821  ///
822  /// Ideally when the caller should use an "atomic write"
823  /// when writing this—write to a temporary file beside the
824  /// lockfile, then rename to overwrite. This will make the
825  /// lockfile more resilient when multiple processes are
826  /// writing to it.
827  pub fn resolve_write_bytes(&mut self) -> Option<Vec<u8>> {
828    if !self.has_content_changed && !self.overwrite {
829      return None;
830    }
831
832    self.has_content_changed = false;
833    Some(self.as_json_string().into_bytes())
834  }
835
836  pub fn remote(&self) -> &BTreeMap<String, String> {
837    &self.content.remote
838  }
839
840  /// Inserts a remote specifier into the lockfile replacing the existing package if it exists.
841  ///
842  /// WARNING: It is up to the caller to ensure checksums of remote modules are
843  /// valid before it is inserted here.
844  pub fn insert_remote(&mut self, specifier: String, hash: String) {
845    let entry = self.content.remote.entry(specifier);
846    match entry {
847      BTreeMapEntry::Vacant(entry) => {
848        entry.insert(hash);
849        self.has_content_changed = true;
850      }
851      BTreeMapEntry::Occupied(mut entry) => {
852        if entry.get() != &hash {
853          entry.insert(hash);
854          self.has_content_changed = true;
855        }
856      }
857    }
858  }
859
860  /// Inserts an npm package into the lockfile replacing the existing package if it exists.
861  ///
862  /// WARNING: It is up to the caller to ensure checksums of packages are
863  /// valid before it is inserted here.
864  pub fn insert_npm_package(&mut self, package_info: NpmPackageLockfileInfo) {
865    let optional_dependencies = package_info
866      .optional_dependencies
867      .into_iter()
868      .map(|dep| (dep.name, dep.id))
869      .collect::<BTreeMap<StackString, StackString>>();
870    let dependencies = package_info
871      .dependencies
872      .into_iter()
873      .map(|dep| (dep.name, dep.id))
874      .collect::<BTreeMap<StackString, StackString>>();
875    let optional_peers = package_info
876      .optional_peers
877      .into_iter()
878      .map(|dep| (dep.name, dep.id))
879      .collect::<BTreeMap<StackString, StackString>>();
880
881    let entry = self.content.packages.npm.entry(package_info.serialized_id);
882    let package_info = NpmPackageInfo {
883      integrity: package_info.integrity,
884      dependencies,
885      optional_dependencies,
886      optional_peers,
887      os: package_info.os,
888      cpu: package_info.cpu,
889      tarball: package_info.tarball,
890      deprecated: package_info.deprecated,
891      scripts: package_info.scripts,
892      bin: package_info.bin,
893    };
894    match entry {
895      BTreeMapEntry::Vacant(entry) => {
896        entry.insert(package_info);
897        self.has_content_changed = true;
898      }
899      BTreeMapEntry::Occupied(mut entry) => {
900        if *entry.get() != package_info {
901          entry.insert(package_info);
902          self.has_content_changed = true;
903        }
904      }
905    }
906  }
907
908  /// Inserts a package specifier into the lockfile.
909  pub fn insert_package_specifier(
910    &mut self,
911    package_req: JsrDepPackageReq,
912    serialized_package_id: SmallStackString,
913  ) {
914    let entry = self.content.packages.specifiers.entry(package_req);
915    match entry {
916      HashMapEntry::Vacant(entry) => {
917        entry.insert(serialized_package_id);
918        self.has_content_changed = true;
919      }
920      HashMapEntry::Occupied(mut entry) => {
921        if *entry.get() != serialized_package_id {
922          entry.insert(serialized_package_id);
923          self.has_content_changed = true;
924        }
925      }
926    }
927  }
928
929  /// Inserts a JSR package into the lockfile replacing the existing package's integrity
930  /// if they differ.
931  ///
932  /// WARNING: It is up to the caller to ensure checksums of packages are
933  /// valid before it is inserted here.
934  pub fn insert_package(&mut self, name: PackageNv, integrity: String) {
935    let entry = self.content.packages.jsr.entry(name);
936    match entry {
937      BTreeMapEntry::Vacant(entry) => {
938        entry.insert(JsrPackageInfo {
939          integrity,
940          dependencies: Default::default(),
941        });
942        self.has_content_changed = true;
943      }
944      BTreeMapEntry::Occupied(mut entry) => {
945        if *entry.get().integrity != integrity {
946          entry.get_mut().integrity = integrity;
947          self.has_content_changed = true;
948        }
949      }
950    }
951  }
952
953  /// Adds package dependencies of a JSR package. This is only used to track
954  /// when packages can be removed from the lockfile.
955  ///
956  /// Note: You MUST insert the package specifiers for any dependencies before
957  /// adding them here as unresolved dependencies will be ignored.
958  pub fn add_package_deps(
959    &mut self,
960    nv: &PackageNv,
961    deps: impl Iterator<Item = JsrDepPackageReq>,
962  ) {
963    if let Some(pkg) = self.content.packages.jsr.get_mut(nv) {
964      let start_count = pkg.dependencies.len();
965      // don't include unresolved dependendencies
966      let resolved_deps =
967        deps.filter(|dep| self.content.packages.specifiers.contains_key(dep));
968      pkg.dependencies.extend(resolved_deps);
969      let end_count = pkg.dependencies.len();
970      if start_count != end_count {
971        self.has_content_changed = true;
972      }
973    }
974  }
975
976  pub fn insert_redirect(&mut self, from: String, to: String) {
977    if from.starts_with("jsr:") {
978      return;
979    }
980
981    let entry = self.content.redirects.entry(from);
982    match entry {
983      BTreeMapEntry::Vacant(entry) => {
984        entry.insert(to);
985        self.has_content_changed = true;
986      }
987      BTreeMapEntry::Occupied(mut entry) => {
988        if *entry.get() != to {
989          entry.insert(to);
990          self.has_content_changed = true;
991        }
992      }
993    }
994  }
995}
996
997#[cfg(test)]
998mod tests {
999  use super::*;
1000  use deno_semver::package::PackageReq;
1001  use futures::FutureExt;
1002  use pretty_assertions::assert_eq;
1003  #[derive(Default)]
1004  struct TestNpmPackageInfoProvider {
1005    cache: HashMap<PackageNv, Lockfile5NpmInfo>,
1006  }
1007
1008  #[derive(Debug)]
1009  struct PackageNotFound(PackageNv);
1010
1011  impl std::fmt::Display for PackageNotFound {
1012    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1013      write!(f, "Package not found: {}", self.0)
1014    }
1015  }
1016
1017  impl std::error::Error for PackageNotFound {}
1018
1019  #[async_trait::async_trait(?Send)]
1020  impl NpmPackageInfoProvider for TestNpmPackageInfoProvider {
1021    async fn get_npm_package_info(
1022      &self,
1023      packages: &[PackageNv],
1024    ) -> Result<Vec<Lockfile5NpmInfo>, Box<dyn std::error::Error + Send + Sync>>
1025    {
1026      let mut infos = Vec::with_capacity(packages.len());
1027      for package in packages {
1028        if let Some(info) = self.cache.get(package) {
1029          infos.push(info.clone());
1030        } else {
1031          return Err(Box::new(PackageNotFound(package.clone())) as _);
1032        }
1033      }
1034      Ok(infos)
1035    }
1036  }
1037
1038  const LOCKFILE_JSON: &str = r#"
1039{
1040  "version": "4",
1041  "npm": {
1042    "nanoid@3.3.4": {
1043      "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="
1044    },
1045    "picocolors@1.0.0": {
1046      "integrity": "sha512-foobar",
1047      "dependencies": []
1048    }
1049  },
1050  "remote": {
1051    "https://deno.land/std@0.71.0/textproto/mod.ts": "3118d7a42c03c242c5a49c2ad91c8396110e14acca1324e7aaefd31a999b71a4",
1052    "https://deno.land/std@0.71.0/async/delay.ts": "35957d585a6e3dd87706858fb1d6b551cb278271b03f52c5a2cb70e65e00c26a"
1053  }
1054}"#;
1055
1056  fn new_lockfile(
1057    options: NewLockfileOptions,
1058  ) -> Result<Lockfile, Box<LockfileError>> {
1059    Lockfile::new(
1060      options,
1061      &TestNpmPackageInfoProvider {
1062        cache: HashMap::from_iter([
1063          (
1064            PackageNv::from_str("nanoid@3.3.4").unwrap(),
1065            Lockfile5NpmInfo {
1066              ..Default::default()
1067            },
1068          ),
1069          (
1070            PackageNv::from_str("picocolors@1.0.0").unwrap(),
1071            Lockfile5NpmInfo {
1072              ..Default::default()
1073            },
1074          ),
1075        ]),
1076      },
1077    )
1078    .now_or_never()
1079    .unwrap()
1080  }
1081  fn setup(overwrite: bool) -> Result<Lockfile, Box<LockfileError>> {
1082    let file_path =
1083      std::env::current_dir().unwrap().join("valid_lockfile.json");
1084    new_lockfile(NewLockfileOptions {
1085      file_path,
1086      content: LOCKFILE_JSON,
1087      overwrite,
1088    })
1089  }
1090
1091  #[test]
1092  fn future_version_unsupported() {
1093    let file_path = PathBuf::from("lockfile.json");
1094    let err = new_lockfile(NewLockfileOptions {
1095      file_path,
1096      content: "{ \"version\": \"2000\" }",
1097      overwrite: false,
1098    })
1099    .unwrap_err();
1100    match err.source {
1101      LockfileErrorReason::UnsupportedVersion { version } => {
1102        assert_eq!(version, "2000")
1103      }
1104      _ => unreachable!(),
1105    }
1106  }
1107
1108  #[test]
1109  fn new_valid_lockfile() {
1110    let lockfile = setup(false).unwrap();
1111
1112    let remote = lockfile.content.remote;
1113    let keys: Vec<String> = remote.keys().cloned().collect();
1114    let expected_keys = vec![
1115      String::from("https://deno.land/std@0.71.0/async/delay.ts"),
1116      String::from("https://deno.land/std@0.71.0/textproto/mod.ts"),
1117    ];
1118
1119    assert_eq!(keys.len(), 2);
1120    assert_eq!(keys, expected_keys);
1121  }
1122
1123  #[test]
1124  fn with_lockfile_content_for_valid_lockfile() {
1125    let file_path = PathBuf::from("/foo");
1126    let result = new_lockfile(NewLockfileOptions {
1127      file_path,
1128      content: LOCKFILE_JSON,
1129      overwrite: false,
1130    })
1131    .unwrap();
1132
1133    let remote = result.content.remote;
1134    let keys: Vec<String> = remote.keys().cloned().collect();
1135    let expected_keys = vec![
1136      String::from("https://deno.land/std@0.71.0/async/delay.ts"),
1137      String::from("https://deno.land/std@0.71.0/textproto/mod.ts"),
1138    ];
1139
1140    assert_eq!(keys.len(), 2);
1141    assert_eq!(keys, expected_keys);
1142  }
1143
1144  #[test]
1145  fn new_lockfile_from_file_and_insert() {
1146    let mut lockfile = setup(false).unwrap();
1147
1148    lockfile.insert_remote(
1149      "https://deno.land/std@0.71.0/io/util.ts".to_string(),
1150      "checksum-1".to_string(),
1151    );
1152
1153    let remote = lockfile.content.remote;
1154    let keys: Vec<String> = remote.keys().cloned().collect();
1155    let expected_keys = vec![
1156      String::from("https://deno.land/std@0.71.0/async/delay.ts"),
1157      String::from("https://deno.land/std@0.71.0/io/util.ts"),
1158      String::from("https://deno.land/std@0.71.0/textproto/mod.ts"),
1159    ];
1160    assert_eq!(keys.len(), 3);
1161    assert_eq!(keys, expected_keys);
1162  }
1163
1164  #[test]
1165  fn new_lockfile_and_write() {
1166    let mut lockfile = setup(true).unwrap();
1167
1168    // true since overwrite was true
1169    assert!(lockfile.resolve_write_bytes().is_some());
1170
1171    lockfile.insert_remote(
1172      "https://deno.land/std@0.71.0/textproto/mod.ts".to_string(),
1173      "checksum-1".to_string(),
1174    );
1175    lockfile.insert_remote(
1176      "https://deno.land/std@0.71.0/io/util.ts".to_string(),
1177      "checksum-2".to_string(),
1178    );
1179    lockfile.insert_remote(
1180      "https://deno.land/std@0.71.0/async/delay.ts".to_string(),
1181      "checksum-3".to_string(),
1182    );
1183
1184    let bytes = lockfile.resolve_write_bytes().unwrap();
1185    let contents_json =
1186      serde_json::from_slice::<serde_json::Value>(&bytes).unwrap();
1187    let object = contents_json["remote"].as_object().unwrap();
1188
1189    assert_eq!(
1190      object
1191        .get("https://deno.land/std@0.71.0/textproto/mod.ts")
1192        .and_then(|v| v.as_str()),
1193      Some("checksum-1")
1194    );
1195
1196    // confirm that keys are sorted alphabetically
1197    let mut keys = object.keys().map(|k| k.as_str());
1198    assert_eq!(
1199      keys.next(),
1200      Some("https://deno.land/std@0.71.0/async/delay.ts")
1201    );
1202    assert_eq!(keys.next(), Some("https://deno.land/std@0.71.0/io/util.ts"));
1203    assert_eq!(
1204      keys.next(),
1205      Some("https://deno.land/std@0.71.0/textproto/mod.ts")
1206    );
1207    assert!(keys.next().is_none());
1208  }
1209
1210  #[test]
1211  fn check_or_insert_lockfile() {
1212    let mut lockfile = setup(false).unwrap();
1213
1214    // none since overwrite was false and there's no changes
1215    assert!(lockfile.resolve_write_bytes().is_none());
1216
1217    lockfile.insert_remote(
1218      "https://deno.land/std@0.71.0/textproto/mod.ts".to_string(),
1219      "checksum-1".to_string(),
1220    );
1221    assert!(lockfile.has_content_changed);
1222
1223    lockfile.has_content_changed = false;
1224    lockfile.insert_remote(
1225      "https://deno.land/std@0.71.0/textproto/mod.ts".to_string(),
1226      "checksum-1".to_string(),
1227    );
1228    assert!(!lockfile.has_content_changed);
1229
1230    lockfile.insert_remote(
1231      "https://deno.land/std@0.71.0/textproto/mod.ts".to_string(),
1232      "checksum-new".to_string(),
1233    );
1234    assert!(lockfile.has_content_changed);
1235    lockfile.has_content_changed = false;
1236
1237    // Not present in lockfile yet, should be inserted and check passed.
1238    lockfile.insert_remote(
1239      "https://deno.land/std@0.71.0/http/file_server.ts".to_string(),
1240      "checksum-1".to_string(),
1241    );
1242    assert!(lockfile.has_content_changed);
1243
1244    // true since there were changes
1245    assert!(lockfile.resolve_write_bytes().is_some());
1246  }
1247
1248  #[test]
1249  fn check_or_insert_lockfile_npm() {
1250    let mut lockfile = setup(false).unwrap();
1251
1252    // already in lockfile
1253    let npm_package = NpmPackageLockfileInfo {
1254      serialized_id: "nanoid@3.3.4".into(),
1255      integrity: Some("sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==".to_string()),
1256      dependencies: vec![],
1257      optional_dependencies: vec![],
1258      optional_peers: vec![],
1259      os: vec![],
1260      cpu: vec![],
1261      tarball: None,
1262      deprecated: false,
1263      scripts: false,
1264      bin: false,
1265    };
1266    lockfile.insert_npm_package(npm_package);
1267    assert!(!lockfile.has_content_changed);
1268
1269    // insert package that exists already, but has slightly different properties
1270    let npm_package = NpmPackageLockfileInfo {
1271      serialized_id: "picocolors@1.0.0".into(),
1272      integrity: Some("sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==".to_string()),
1273      dependencies: vec![],
1274      optional_dependencies: vec![],
1275      optional_peers: vec![],
1276      os: vec![],
1277      cpu: vec![],
1278      tarball: None,
1279      deprecated: false,
1280      scripts: false,
1281      bin: false,
1282    };
1283    lockfile.insert_npm_package(npm_package);
1284    assert!(lockfile.has_content_changed);
1285
1286    lockfile.has_content_changed = false;
1287    let npm_package = NpmPackageLockfileInfo {
1288      serialized_id: "source-map-js@1.0.2".into(),
1289      integrity: Some("sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==".to_string()),
1290      dependencies: vec![],
1291      optional_dependencies: vec![],
1292      optional_peers: vec![],
1293      os: vec![],
1294      cpu: vec![],
1295      tarball: None,
1296      deprecated: false,
1297      scripts: false,
1298      bin: false,
1299    };
1300    // Not present in lockfile yet, should be inserted
1301    lockfile.insert_npm_package(npm_package.clone());
1302    assert!(lockfile.has_content_changed);
1303    lockfile.has_content_changed = false;
1304
1305    // this one should not say the lockfile has changed because it's the same
1306    lockfile.insert_npm_package(npm_package);
1307    assert!(!lockfile.has_content_changed);
1308
1309    let npm_package = NpmPackageLockfileInfo {
1310      serialized_id: "source-map-js@1.0.2".into(),
1311      integrity: Some("sha512-foobar".to_string()),
1312      dependencies: vec![],
1313      optional_dependencies: vec![],
1314      optional_peers: vec![],
1315      os: vec![],
1316      cpu: vec![],
1317      tarball: None,
1318      deprecated: false,
1319      scripts: false,
1320      bin: false,
1321    };
1322    // Now present in lockfile, should be changed due to different integrity
1323    lockfile.insert_npm_package(npm_package);
1324    assert!(lockfile.has_content_changed);
1325  }
1326
1327  #[test]
1328  fn lockfile_with_redirects() {
1329    let mut lockfile = new_lockfile(NewLockfileOptions {
1330      file_path: PathBuf::from("/foo/deno.lock"),
1331      content: r#"{
1332  "version": "4",
1333  "redirects": {
1334    "https://deno.land/x/std/mod.ts": "https://deno.land/std@0.190.0/mod.ts"
1335  }
1336}"#,
1337
1338      overwrite: false,
1339    })
1340    .unwrap();
1341    lockfile.content.redirects.insert(
1342      "https://deno.land/x/other/mod.ts".to_string(),
1343      "https://deno.land/x/other@0.1.0/mod.ts".to_string(),
1344    );
1345    assert_eq!(
1346      lockfile.as_json_string(),
1347      r#"{
1348  "version": "5",
1349  "redirects": {
1350    "https://deno.land/x/other/mod.ts": "https://deno.land/x/other@0.1.0/mod.ts",
1351    "https://deno.land/x/std/mod.ts": "https://deno.land/std@0.190.0/mod.ts"
1352  }
1353}
1354"#,
1355    );
1356  }
1357
1358  #[test]
1359  fn test_insert_redirect() {
1360    let mut lockfile = new_lockfile(NewLockfileOptions {
1361      file_path: PathBuf::from("/foo/deno.lock"),
1362      content: r#"{
1363  "version": "4",
1364  "redirects": {
1365    "https://deno.land/x/std/mod.ts": "https://deno.land/std@0.190.0/mod.ts"
1366  }
1367}"#,
1368      overwrite: false,
1369    })
1370    .unwrap();
1371    lockfile.insert_redirect(
1372      "https://deno.land/x/std/mod.ts".to_string(),
1373      "https://deno.land/std@0.190.0/mod.ts".to_string(),
1374    );
1375    assert!(!lockfile.has_content_changed);
1376    lockfile.insert_redirect(
1377      "https://deno.land/x/std/mod.ts".to_string(),
1378      "https://deno.land/std@0.190.1/mod.ts".to_string(),
1379    );
1380    assert!(lockfile.has_content_changed);
1381    lockfile.insert_redirect(
1382      "https://deno.land/x/std/other.ts".to_string(),
1383      "https://deno.land/std@0.190.1/other.ts".to_string(),
1384    );
1385    assert_eq!(
1386      lockfile.as_json_string(),
1387      r#"{
1388  "version": "5",
1389  "redirects": {
1390    "https://deno.land/x/std/mod.ts": "https://deno.land/std@0.190.1/mod.ts",
1391    "https://deno.land/x/std/other.ts": "https://deno.land/std@0.190.1/other.ts"
1392  }
1393}
1394"#,
1395    );
1396  }
1397
1398  #[test]
1399  fn test_insert_jsr() {
1400    let mut lockfile = new_lockfile(NewLockfileOptions {
1401      file_path: PathBuf::from("/foo/deno.lock"),
1402      content: r#"{
1403  "version": "4",
1404  "specifiers": {
1405    "jsr:path": "jsr:@std/path@0.75.0"
1406  }
1407}"#,
1408      overwrite: false,
1409    })
1410    .unwrap();
1411    lockfile.insert_package_specifier(
1412      JsrDepPackageReq::jsr(PackageReq::from_str("path").unwrap()),
1413      "jsr:@std/path@0.75.0".into(),
1414    );
1415    assert!(!lockfile.has_content_changed);
1416    lockfile.insert_package_specifier(
1417      JsrDepPackageReq::jsr(PackageReq::from_str("path").unwrap()),
1418      "jsr:@std/path@0.75.1".into(),
1419    );
1420    assert!(lockfile.has_content_changed);
1421    lockfile.insert_package_specifier(
1422      JsrDepPackageReq::jsr(PackageReq::from_str("@foo/bar@^2").unwrap()),
1423      "jsr:@foo/bar@2.1.2".into(),
1424    );
1425    assert_eq!(
1426      lockfile.as_json_string(),
1427      r#"{
1428  "version": "5",
1429  "specifiers": {
1430    "jsr:@foo/bar@2": "jsr:@foo/bar@2.1.2",
1431    "jsr:path@*": "jsr:@std/path@0.75.1"
1432  }
1433}
1434"#,
1435    );
1436  }
1437
1438  #[test]
1439  fn read_version_1() {
1440    let content: &str = r#"{
1441      "https://deno.land/std@0.71.0/textproto/mod.ts": "3118d7a42c03c242c5a49c2ad91c8396110e14acca1324e7aaefd31a999b71a4",
1442      "https://deno.land/std@0.71.0/async/delay.ts": "35957d585a6e3dd87706858fb1d6b551cb278271b03f52c5a2cb70e65e00c26a"
1443    }"#;
1444    let file_path = PathBuf::from("lockfile.json");
1445    let lockfile = new_lockfile(NewLockfileOptions {
1446      file_path,
1447      content,
1448      overwrite: false,
1449    })
1450    .unwrap();
1451    assert_eq!(lockfile.content.remote.len(), 2);
1452  }
1453
1454  #[test]
1455  fn read_version_2() {
1456    let content: &str = r#"{
1457      "version": "2",
1458      "remote": {
1459        "https://deno.land/std@0.71.0/textproto/mod.ts": "3118d7a42c03c242c5a49c2ad91c8396110e14acca1324e7aaefd31a999b71a4",
1460        "https://deno.land/std@0.71.0/async/delay.ts": "35957d585a6e3dd87706858fb1d6b551cb278271b03f52c5a2cb70e65e00c26a"
1461      },
1462      "npm": {
1463        "specifiers": {
1464          "nanoid": "nanoid@3.3.4"
1465        },
1466        "packages": {
1467          "nanoid@3.3.4": {
1468            "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
1469            "dependencies": {}
1470          },
1471          "picocolors@1.0.0": {
1472            "integrity": "sha512-foobar",
1473            "dependencies": {}
1474          }
1475        }
1476      }
1477    }"#;
1478    let file_path = PathBuf::from("lockfile.json");
1479    let lockfile = new_lockfile(NewLockfileOptions {
1480      file_path,
1481      content,
1482      overwrite: false,
1483    })
1484    .unwrap();
1485    assert_eq!(lockfile.content.packages.npm.len(), 2);
1486    assert_eq!(
1487      lockfile.content.packages.specifiers,
1488      HashMap::from([(
1489        JsrDepPackageReq::npm(PackageReq::from_str("nanoid").unwrap()),
1490        "3.3.4".into()
1491      )])
1492    );
1493    assert_eq!(lockfile.content.remote.len(), 2);
1494  }
1495
1496  #[test]
1497  fn insert_package_deps_changes_empty_insert() {
1498    let content: &str = r#"{
1499      "version": "2",
1500      "remote": {}
1501    }"#;
1502    let file_path = PathBuf::from("lockfile.json");
1503    let mut lockfile = new_lockfile(NewLockfileOptions {
1504      file_path,
1505      content,
1506      overwrite: false,
1507    })
1508    .unwrap();
1509
1510    lockfile.insert_package_specifier(
1511      JsrDepPackageReq::jsr(PackageReq::from_str("dep2").unwrap()),
1512      "dep2@1.0.0".into(),
1513    );
1514    assert!(lockfile.has_content_changed);
1515    lockfile.has_content_changed = false;
1516
1517    assert!(!lockfile.has_content_changed);
1518    let dep_nv = PackageNv::from_str("dep@1.0.0").unwrap();
1519    lockfile.insert_package(dep_nv.clone(), "integrity".to_string());
1520    // has changed even though it was empty
1521    assert!(lockfile.has_content_changed);
1522
1523    // now try inserting the same package
1524    lockfile.has_content_changed = false;
1525    lockfile.insert_package(dep_nv.clone(), "integrity".to_string());
1526    assert!(!lockfile.has_content_changed);
1527
1528    // now with new deps
1529    lockfile.add_package_deps(
1530      &dep_nv,
1531      vec![JsrDepPackageReq::jsr(PackageReq::from_str("dep2").unwrap())]
1532        .into_iter(),
1533    );
1534    assert!(lockfile.has_content_changed);
1535    lockfile.has_content_changed = false;
1536
1537    // now insert a dep that doesn't have a package specifier
1538    lockfile.add_package_deps(
1539      &dep_nv,
1540      vec![JsrDepPackageReq::jsr(
1541        PackageReq::from_str("dep-non-resolved").unwrap(),
1542      )]
1543      .into_iter(),
1544    );
1545    assert!(!lockfile.has_content_changed);
1546  }
1547
1548  #[test]
1549  fn empty_lockfile_nicer_error() {
1550    let content: &str = r#"  "#;
1551    let file_path = PathBuf::from("lockfile.json");
1552    let err = new_lockfile(NewLockfileOptions {
1553      file_path,
1554      content,
1555      overwrite: false,
1556    })
1557    .err()
1558    .unwrap();
1559    assert!(matches!(err.source, LockfileErrorReason::Empty));
1560  }
1561}