deno_npm/
lib.rs

1// Copyright 2018-2024 the Deno authors. MIT license.
2
3#![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      // unfortunately we can't do something like `_3` when
84      // this gets deep because npm package names can start
85      // with a number
86      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/// A resolved unique identifier for an npm package. This contains
104/// the resolved name, version, and peer dependency resolution identifiers.
105#[derive(
106  Clone, PartialEq, Eq, Hash, Serialize, Deserialize, CapacityDisplay,
107)]
108pub struct NpmPackageId {
109  pub nv: PackageNv,
110  pub peer_dependencies: NpmPackageIdPeerDependencies,
111}
112
113// Custom debug implementation for more concise test output
114impl 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    // WARNING: This should not change because it's used in the lockfile
131    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          // first character might be a scope, so skip it
152          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      // todo: improve monch to provide the error message without source
170      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/// Represents an npm package as it might be found in a cache folder
268/// where duplicate copies of the same package may exist.
269#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
270pub struct NpmPackageCacheFolderId {
271  pub nv: PackageNv,
272  /// Peer dependency resolution may require us to have duplicate copies
273  /// of the same package.
274  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  /// The peer dependency resolution can differ for the same
320  /// package (name and version) depending on where it is in
321  /// the resolution tree. This copy index indicates which
322  /// copy of the package this is.
323  pub copy_index: u8,
324  #[serde(flatten)]
325  pub system: NpmResolutionPackageSystemInfo,
326  pub dist: NpmPackageVersionDistInfo,
327  /// Key is what the package refers to the other package as,
328  /// which could be different from the package name.
329  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    // custom debug implementation for deterministic output in the tests
339    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/// System information used to determine which optional packages
381/// to download.
382#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
383pub struct NpmSystemInfo {
384  /// `process.platform` value from Node.js
385  pub os: SmallStackString,
386  /// `process.arch` value from Node.js
387  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  // possible values: https://nodejs.org/api/process.html#processarch
428  // 'arm', 'arm64', 'ia32', 'mips','mipsel', 'ppc', 'ppc64', 's390', 's390x', and 'x64'
429  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  // possible values: https://nodejs.org/api/process.html#processplatform
438  // 'aix', 'darwin', 'freebsd', 'linux', 'openbsd', 'sunos', and 'win32'
439  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    // this shouldn't change because it's used in the lockfile
481    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    // not explicitly excluded and there's an include, so it's considered a match
546    assert!(matches_os_or_cpu_vec(
547      &["!arm64".into(), "!x86".into(), "other".into()],
548      "x64"
549    ));
550  }
551}