1#![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 pub no_config: bool,
44 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 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 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 pub dependencies: HashSet<JsrDepPackageReq>,
128}
129
130#[derive(Clone, Debug, Default)]
131pub struct PackagesContent {
132 pub specifiers: HashMap<JsrDepPackageReq, SmallStackString>,
139
140 pub jsr: BTreeMap<PackageNv, JsrPackageInfo>,
151
152 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 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 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 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 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 let mut to_resolved_specifiers: HashMap<
424 Cow<JsrDepPackageReq>,
425 &JsrDepPackageReq,
426 > = HashMap::with_capacity(specifiers.len() * 2);
427 for dep in specifiers.keys() {
429 to_resolved_specifiers.insert(Cow::Borrowed(dep), dep);
430 }
431 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; };
438 let entry =
439 to_resolved_specifiers.entry(Cow::Owned(dep_no_version_req));
440 if let HashMapEntry::Vacant(entry) = entry {
444 entry.insert(dep);
445 }
446 }
447
448 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; };
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 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 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 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 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 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 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 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 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 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 let mut graph = LockfilePackageGraph::from_lockfile(
798 packages,
799 remotes,
800 old_deps.into_iter(),
801 );
802
803 graph.remove_root_packages(removed_deps.into_iter());
805
806 graph.populate_packages(
808 &mut self.content.packages,
809 &mut self.content.remote,
810 );
811 }
812
813 if !allow_content_changed {
814 self.has_content_changed = false;
817 }
818 }
819
820 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 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 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 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 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 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 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 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 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 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 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 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 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 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 lockfile.insert_npm_package(npm_package.clone());
1302 assert!(lockfile.has_content_changed);
1303 lockfile.has_content_changed = false;
1304
1305 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 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 assert!(lockfile.has_content_changed);
1522
1523 lockfile.has_content_changed = false;
1525 lockfile.insert_package(dep_nv.clone(), "integrity".to_string());
1526 assert!(!lockfile.has_content_changed);
1527
1528 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 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}