1#![deny(clippy::print_stderr)]
4#![deny(clippy::print_stdout)]
5#![deny(clippy::unused_async)]
6
7use std::cmp::Ordering;
8use std::collections::BTreeMap;
9use std::collections::HashMap;
10use std::collections::HashSet;
11
12use capacity_builder::CapacityDisplay;
13use capacity_builder::StringAppendable;
14use capacity_builder::StringBuilder;
15use deno_error::JsError;
16use deno_semver::package::PackageNv;
17use deno_semver::CowVec;
18use deno_semver::SmallStackString;
19use deno_semver::StackString;
20use deno_semver::Version;
21use registry::NpmPackageVersionBinEntry;
22use registry::NpmPackageVersionDistInfo;
23use resolution::SerializedNpmResolutionSnapshotPackage;
24use serde::Deserialize;
25use serde::Serialize;
26use thiserror::Error;
27
28pub mod npm_rc;
29pub mod registry;
30pub mod resolution;
31
32#[derive(Debug, Error, Clone, JsError)]
33#[class(type)]
34#[error("Invalid npm package id '{text}'. {message}")]
35pub struct NpmPackageIdDeserializationError {
36 message: String,
37 text: String,
38}
39
40#[derive(
41 Clone,
42 Default,
43 PartialEq,
44 Eq,
45 Hash,
46 Serialize,
47 Deserialize,
48 PartialOrd,
49 Ord,
50 CapacityDisplay,
51)]
52pub struct NpmPackageIdPeerDependencies(CowVec<NpmPackageId>);
53
54impl<const N: usize> From<[NpmPackageId; N]> for NpmPackageIdPeerDependencies {
55 fn from(value: [NpmPackageId; N]) -> Self {
56 Self(CowVec::from(value))
57 }
58}
59
60impl NpmPackageIdPeerDependencies {
61 pub fn with_capacity(capacity: usize) -> Self {
62 Self(CowVec::with_capacity(capacity))
63 }
64
65 pub fn as_serialized(&self) -> StackString {
66 capacity_builder::appendable_to_string(self)
67 }
68
69 pub fn push(&mut self, id: NpmPackageId) {
70 self.0.push(id);
71 }
72
73 pub fn iter(&self) -> impl Iterator<Item = &NpmPackageId> {
74 self.0.iter()
75 }
76
77 fn peer_serialized_with_level<'a, TString: capacity_builder::StringType>(
78 &'a self,
79 builder: &mut StringBuilder<'a, TString>,
80 level: usize,
81 ) {
82 for peer in &self.0 {
83 for _ in 0..level + 1 {
87 builder.append('_');
88 }
89 peer.as_serialized_with_level(builder, level + 1);
90 }
91 }
92}
93
94impl<'a> StringAppendable<'a> for &'a NpmPackageIdPeerDependencies {
95 fn append_to_builder<TString: capacity_builder::StringType>(
96 self,
97 builder: &mut StringBuilder<'a, TString>,
98 ) {
99 self.peer_serialized_with_level(builder, 0)
100 }
101}
102
103#[derive(
106 Clone, PartialEq, Eq, Hash, Serialize, Deserialize, CapacityDisplay,
107)]
108pub struct NpmPackageId {
109 pub nv: PackageNv,
110 pub peer_dependencies: NpmPackageIdPeerDependencies,
111}
112
113impl std::fmt::Debug for NpmPackageId {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 write!(f, "{}", self.as_serialized())
117 }
118}
119
120impl NpmPackageId {
121 pub fn as_serialized(&self) -> StackString {
122 capacity_builder::appendable_to_string(self)
123 }
124
125 fn as_serialized_with_level<'a, TString: capacity_builder::StringType>(
126 &'a self,
127 builder: &mut StringBuilder<'a, TString>,
128 level: usize,
129 ) {
130 if level == 0 {
132 builder.append(self.nv.name.as_str());
133 } else {
134 builder.append_with_replace(self.nv.name.as_str(), "/", "+");
135 }
136 builder.append('@');
137 builder.append(&self.nv.version);
138 self
139 .peer_dependencies
140 .peer_serialized_with_level(builder, level);
141 }
142
143 pub fn from_serialized(
144 id: &str,
145 ) -> Result<Self, NpmPackageIdDeserializationError> {
146 use monch::*;
147
148 fn parse_name(input: &str) -> ParseResult<&str> {
149 if_not_empty(substring(move |input| {
150 for (pos, c) in input.char_indices() {
151 if pos > 0 && c == '@' {
153 return Ok((&input[pos..], ()));
154 }
155 }
156 ParseError::backtrace()
157 }))(input)
158 }
159
160 fn parse_version(input: &str) -> ParseResult<&str> {
161 if_not_empty(substring(skip_while(|c| c != '_')))(input)
162 }
163
164 fn parse_name_and_version(input: &str) -> ParseResult<(&str, Version)> {
165 let (input, name) = parse_name(input)?;
166 let (input, _) = ch('@')(input)?;
167 let at_version_input = input;
168 let (input, version) = parse_version(input)?;
169 match Version::parse_from_npm(version) {
171 Ok(version) => Ok((input, (name, version))),
172 Err(err) => ParseError::fail(
173 at_version_input,
174 format!("Invalid npm version. {}", err.message()),
175 ),
176 }
177 }
178
179 fn parse_level_at_level<'a>(
180 level: usize,
181 ) -> impl Fn(&'a str) -> ParseResult<'a, ()> {
182 fn parse_level(input: &str) -> ParseResult<usize> {
183 let level = input.chars().take_while(|c| *c == '_').count();
184 Ok((&input[level..], level))
185 }
186
187 move |input| {
188 let (input, parsed_level) = parse_level(input)?;
189 if parsed_level == level {
190 Ok((input, ()))
191 } else {
192 ParseError::backtrace()
193 }
194 }
195 }
196
197 fn parse_peers_at_level<'a>(
198 level: usize,
199 ) -> impl Fn(&'a str) -> ParseResult<'a, CowVec<NpmPackageId>> {
200 move |mut input| {
201 let mut peers = CowVec::new();
202 while let Ok((level_input, _)) = parse_level_at_level(level)(input) {
203 input = level_input;
204 let peer_result = parse_id_at_level(level)(input)?;
205 input = peer_result.0;
206 peers.push(peer_result.1);
207 }
208 Ok((input, peers))
209 }
210 }
211
212 fn parse_id_at_level<'a>(
213 level: usize,
214 ) -> impl Fn(&'a str) -> ParseResult<'a, NpmPackageId> {
215 move |input| {
216 let (input, (name, version)) = parse_name_and_version(input)?;
217 let name = if level > 0 {
218 StackString::from_str(name).replace("+", "/")
219 } else {
220 StackString::from_str(name)
221 };
222 let (input, peer_dependencies) =
223 parse_peers_at_level(level + 1)(input)?;
224 Ok((
225 input,
226 NpmPackageId {
227 nv: PackageNv { name, version },
228 peer_dependencies: NpmPackageIdPeerDependencies(peer_dependencies),
229 },
230 ))
231 }
232 }
233
234 with_failure_handling(parse_id_at_level(0))(id).map_err(|err| {
235 NpmPackageIdDeserializationError {
236 message: format!("{err:#}"),
237 text: id.to_string(),
238 }
239 })
240 }
241}
242
243impl<'a> capacity_builder::StringAppendable<'a> for &'a NpmPackageId {
244 fn append_to_builder<TString: capacity_builder::StringType>(
245 self,
246 builder: &mut capacity_builder::StringBuilder<'a, TString>,
247 ) {
248 self.as_serialized_with_level(builder, 0)
249 }
250}
251
252impl Ord for NpmPackageId {
253 fn cmp(&self, other: &Self) -> Ordering {
254 match self.nv.cmp(&other.nv) {
255 Ordering::Equal => self.peer_dependencies.cmp(&other.peer_dependencies),
256 ordering => ordering,
257 }
258 }
259}
260
261impl PartialOrd for NpmPackageId {
262 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
263 Some(self.cmp(other))
264 }
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
270pub struct NpmPackageCacheFolderId {
271 pub nv: PackageNv,
272 pub copy_index: u8,
275}
276
277impl NpmPackageCacheFolderId {
278 pub fn with_no_count(&self) -> Self {
279 Self {
280 nv: self.nv.clone(),
281 copy_index: 0,
282 }
283 }
284}
285
286impl std::fmt::Display for NpmPackageCacheFolderId {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 write!(f, "{}", self.nv)?;
289 if self.copy_index > 0 {
290 write!(f, "_{}", self.copy_index)?;
291 }
292 Ok(())
293 }
294}
295
296#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
297pub struct NpmResolutionPackageSystemInfo {
298 pub os: Vec<SmallStackString>,
299 pub cpu: Vec<SmallStackString>,
300}
301
302impl NpmResolutionPackageSystemInfo {
303 pub fn matches_system(&self, system_info: &NpmSystemInfo) -> bool {
304 self.matches_cpu(&system_info.cpu) && self.matches_os(&system_info.os)
305 }
306
307 pub fn matches_cpu(&self, target: &str) -> bool {
308 matches_os_or_cpu_vec(&self.cpu, target)
309 }
310
311 pub fn matches_os(&self, target: &str) -> bool {
312 matches_os_or_cpu_vec(&self.os, target)
313 }
314}
315
316#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
317pub struct NpmResolutionPackage {
318 pub id: NpmPackageId,
319 pub copy_index: u8,
324 #[serde(flatten)]
325 pub system: NpmResolutionPackageSystemInfo,
326 pub dist: NpmPackageVersionDistInfo,
327 pub dependencies: HashMap<StackString, NpmPackageId>,
330 pub optional_dependencies: HashSet<StackString>,
331 pub bin: Option<NpmPackageVersionBinEntry>,
332 pub scripts: HashMap<SmallStackString, String>,
333 pub deprecated: Option<String>,
334}
335
336impl std::fmt::Debug for NpmResolutionPackage {
337 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338 f.debug_struct("NpmResolutionPackage")
340 .field("pkg_id", &self.id)
341 .field("copy_index", &self.copy_index)
342 .field("system", &self.system)
343 .field("dist", &self.dist)
344 .field(
345 "dependencies",
346 &self.dependencies.iter().collect::<BTreeMap<_, _>>(),
347 )
348 .field("optional_dependencies", &{
349 let mut deps = self.optional_dependencies.iter().collect::<Vec<_>>();
350 deps.sort();
351 deps
352 })
353 .field("deprecated", &self.deprecated)
354 .finish()
355 }
356}
357
358impl NpmResolutionPackage {
359 pub fn as_serialized(&self) -> SerializedNpmResolutionSnapshotPackage {
360 SerializedNpmResolutionSnapshotPackage {
361 id: self.id.clone(),
362 system: self.system.clone(),
363 dist: self.dist.clone(),
364 dependencies: self.dependencies.clone(),
365 optional_dependencies: self.optional_dependencies.clone(),
366 bin: self.bin.clone(),
367 scripts: self.scripts.clone(),
368 deprecated: self.deprecated.clone(),
369 }
370 }
371
372 pub fn get_package_cache_folder_id(&self) -> NpmPackageCacheFolderId {
373 NpmPackageCacheFolderId {
374 nv: self.id.nv.clone(),
375 copy_index: self.copy_index,
376 }
377 }
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
383pub struct NpmSystemInfo {
384 pub os: SmallStackString,
386 pub cpu: SmallStackString,
388}
389
390impl Default for NpmSystemInfo {
391 fn default() -> Self {
392 Self {
393 os: node_js_os(std::env::consts::OS).into(),
394 cpu: node_js_cpu(std::env::consts::ARCH).into(),
395 }
396 }
397}
398
399impl NpmSystemInfo {
400 pub fn from_rust(os: &str, cpu: &str) -> Self {
401 Self {
402 os: node_js_os(os).into(),
403 cpu: node_js_cpu(cpu).into(),
404 }
405 }
406}
407
408fn matches_os_or_cpu_vec(items: &[SmallStackString], target: &str) -> bool {
409 if items.is_empty() {
410 return true;
411 }
412 let mut had_negation = false;
413 for item in items {
414 if item.starts_with('!') {
415 if &item[1..] == target {
416 return false;
417 }
418 had_negation = true;
419 } else if item == target {
420 return true;
421 }
422 }
423 had_negation
424}
425
426fn node_js_cpu(rust_arch: &str) -> &str {
427 match rust_arch {
430 "x86_64" => "x64",
431 "aarch64" => "arm64",
432 value => value,
433 }
434}
435
436fn node_js_os(rust_os: &str) -> &str {
437 match rust_os {
440 "macos" => "darwin",
441 "windows" => "win32",
442 value => value,
443 }
444}
445
446#[cfg(test)]
447mod test {
448 use super::*;
449
450 #[test]
451 fn serialize_npm_package_id() {
452 let id = NpmPackageId {
453 nv: PackageNv::from_str("pkg-a@1.2.3").unwrap(),
454 peer_dependencies: NpmPackageIdPeerDependencies::from([
455 NpmPackageId {
456 nv: PackageNv::from_str("pkg-b@3.2.1").unwrap(),
457 peer_dependencies: NpmPackageIdPeerDependencies::from([
458 NpmPackageId {
459 nv: PackageNv::from_str("pkg-c@1.3.2").unwrap(),
460 peer_dependencies: Default::default(),
461 },
462 NpmPackageId {
463 nv: PackageNv::from_str("pkg-d@2.3.4").unwrap(),
464 peer_dependencies: Default::default(),
465 },
466 ]),
467 },
468 NpmPackageId {
469 nv: PackageNv::from_str("pkg-e@2.3.1").unwrap(),
470 peer_dependencies: NpmPackageIdPeerDependencies::from([
471 NpmPackageId {
472 nv: PackageNv::from_str("pkg-f@2.3.1").unwrap(),
473 peer_dependencies: Default::default(),
474 },
475 ]),
476 },
477 ]),
478 };
479
480 let serialized = id.as_serialized();
482 assert_eq!(serialized, "pkg-a@1.2.3_pkg-b@3.2.1__pkg-c@1.3.2__pkg-d@2.3.4_pkg-e@2.3.1__pkg-f@2.3.1");
483 assert_eq!(NpmPackageId::from_serialized(&serialized).unwrap(), id);
484 }
485
486 #[test]
487 fn parse_npm_package_id() {
488 #[track_caller]
489 fn run_test(input: &str) {
490 let id = NpmPackageId::from_serialized(input).unwrap();
491 assert_eq!(id.as_serialized(), input);
492 }
493
494 run_test("pkg-a@1.2.3");
495 run_test("pkg-a@1.2.3_pkg-b@3.2.1");
496 run_test(
497 "pkg-a@1.2.3_pkg-b@3.2.1__pkg-c@1.3.2__pkg-d@2.3.4_pkg-e@2.3.1__pkg-f@2.3.1",
498 );
499
500 #[track_caller]
501 fn run_error_test(input: &str, message: &str) {
502 let err = NpmPackageId::from_serialized(input).unwrap_err();
503 assert_eq!(format!("{:#}", err), message);
504 }
505
506 run_error_test(
507 "asdf",
508 "Invalid npm package id 'asdf'. Unexpected character.
509 asdf
510 ~",
511 );
512 run_error_test(
513 "asdf@test",
514 "Invalid npm package id 'asdf@test'. Invalid npm version. Unexpected character.
515 test
516 ~",
517 );
518 run_error_test(
519 "pkg@1.2.3_asdf@test",
520 "Invalid npm package id 'pkg@1.2.3_asdf@test'. Invalid npm version. Unexpected character.
521 test
522 ~",
523 );
524 }
525
526 #[test]
527 fn test_matches_os_or_cpu_vec() {
528 assert!(matches_os_or_cpu_vec(&[], "x64"));
529 assert!(matches_os_or_cpu_vec(&["x64".into()], "x64"));
530 assert!(!matches_os_or_cpu_vec(&["!x64".into()], "x64"));
531 assert!(matches_os_or_cpu_vec(&["!arm64".into()], "x64"));
532 assert!(matches_os_or_cpu_vec(
533 &["!arm64".into(), "!x86".into()],
534 "x64"
535 ));
536 assert!(!matches_os_or_cpu_vec(
537 &["!arm64".into(), "!x86".into()],
538 "x86"
539 ));
540 assert!(!matches_os_or_cpu_vec(
541 &["!arm64".into(), "!x86".into(), "other".into()],
542 "x86"
543 ));
544
545 assert!(matches_os_or_cpu_vec(
547 &["!arm64".into(), "!x86".into(), "other".into()],
548 "x64"
549 ));
550 }
551}