├── .gitignore ├── .rustfmt.toml ├── rust-toolchain.toml ├── src ├── resolution │ ├── tracing │ │ ├── deno.json │ │ ├── README.md │ │ ├── app.css │ │ ├── mod.rs │ │ ├── deno.lock │ │ └── app.js │ ├── mod.rs │ ├── collections.rs │ └── common.rs ├── npm_rc │ ├── ini.rs │ └── mod.rs ├── lib.rs └── registry.rs ├── README.md ├── .github └── workflows │ ├── publish.yml │ ├── ci.yml │ └── release.yml ├── LICENSE ├── Cargo.toml ├── benches └── bench.rs └── examples └── min_repro_solver.rs /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | /target 3 | .bench-reg 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | tab_spaces = 2 3 | edition = "2024" 4 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.89.0" 3 | components = ["clippy", "rustfmt"] 4 | profile = "minimal" 5 | -------------------------------------------------------------------------------- /src/resolution/tracing/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": false, 4 | "lib": ["esnext", "DOM"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deno_npm 2 | 3 | [![](https://img.shields.io/crates/v/deno_npm.svg)](https://crates.io/crates/deno_npm) 4 | 5 | npm registry client and dependency resolver used in the Deno CLI. 6 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | id-token: write 14 | steps: 15 | - name: Clone repository 16 | uses: actions/checkout@v5 17 | - uses: rust-lang/crates-io-auth-action@v1 18 | id: auth 19 | - run: cargo publish 20 | env: 21 | CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} 22 | -------------------------------------------------------------------------------- /src/resolution/tracing/README.md: -------------------------------------------------------------------------------- 1 | # tracing 2 | 3 | This is a tool for debugging the npm resolution. 4 | 5 | To use it, compile with `--feature tracing`. For example: 6 | 7 | ```sh 8 | cargo test grand_child_package_has_self_as_peer_dependency_root --features tracing -- --nocapture 9 | ``` 10 | 11 | This will output something like: 12 | 13 | ``` 14 | ============== 15 | Trace output ready! Please open your browser to: file:///.../deno-npm-trace.html 16 | ============== 17 | ``` 18 | 19 | Follow that and open your browser to see the output. 20 | -------------------------------------------------------------------------------- /src/resolution/tracing/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | height: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | overflow: hidden; 8 | } 9 | 10 | #container { 11 | display: flex; 12 | flex-direction: column; 13 | height: 100vh; 14 | } 15 | 16 | #slider { 17 | display: flex; 18 | align-items: center; 19 | gap: 10px; 20 | padding: 10px; 21 | background: #f0f0f0; 22 | } 23 | 24 | #slider input[type="range"] { 25 | flex: 1; 26 | min-width: 0; 27 | } 28 | 29 | #stepText { 30 | flex: 0; 31 | white-space: nowrap; 32 | } 33 | 34 | #main { 35 | display: flex; 36 | flex: 1; 37 | flex-direction: row; 38 | height: 100vh; 39 | } 40 | 41 | #graph { 42 | flex: 1; 43 | overflow: hidden; 44 | } 45 | 46 | #info h3 { 47 | padding-top: 0; 48 | margin-top: 0; 49 | } 50 | 51 | #info { 52 | padding: 20px; 53 | background: #f0f0f0; 54 | flex: 0 0 400px; 55 | overflow-y: scroll; 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | rust: 12 | name: deno_npm-ubuntu-latest-release 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 30 15 | 16 | env: 17 | CARGO_INCREMENTAL: 0 18 | GH_ACTIONS: 1 19 | RUST_BACKTRACE: full 20 | RUSTFLAGS: -D warnings 21 | 22 | steps: 23 | - name: Clone repository 24 | uses: actions/checkout@v5 25 | 26 | - uses: dsherret/rust-toolchain-file@v1 27 | 28 | - uses: Swatinem/rust-cache@v2 29 | with: 30 | save-if: ${{ github.ref == 'refs/heads/main' }} 31 | 32 | - name: Format 33 | run: cargo fmt --all -- --check 34 | 35 | - name: Lint 36 | run: cargo clippy --all-targets --all-features --release 37 | 38 | - name: Build 39 | run: cargo build --all-targets --all-features --release 40 | - name: Test 41 | run: cargo test --all-targets --all-features --release 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | releaseKind: 7 | description: "Kind of release" 8 | default: "minor" 9 | type: choice 10 | options: 11 | - patch 12 | - minor 13 | required: true 14 | 15 | jobs: 16 | rust: 17 | name: release 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 30 20 | 21 | steps: 22 | - name: Clone repository 23 | uses: actions/checkout@v5 24 | with: 25 | token: ${{ secrets.DENOBOT_PAT }} 26 | 27 | - uses: denoland/setup-deno@v2 28 | - uses: dsherret/rust-toolchain-file@v1 29 | 30 | - name: Tag and release 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.DENOBOT_PAT }} 33 | GH_WORKFLOW_ACTOR: ${{ github.actor }} 34 | run: | 35 | git config user.email "denobot@users.noreply.github.com" 36 | git config user.name "denobot" 37 | deno run -A jsr:@deno/rust-automation/tasks/publish-release --${{github.event.inputs.releaseKind}} 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2024 the Deno authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deno_npm" 3 | version = "0.42.2" 4 | edition = "2024" 5 | description = "npm registry client and dependency resolver used in the Deno CLI" 6 | homepage = "https://deno.land/" 7 | repository = "https://github.com/denoland/deno_npm" 8 | documentation = "https://docs.rs/deno_npm" 9 | authors = ["the Deno authors"] 10 | license = "MIT" 11 | 12 | [features] 13 | tracing = [] 14 | 15 | [dependencies] 16 | async-trait = "0.1.68" 17 | capacity_builder = "0.5.0" 18 | chrono = { version = "0.4.42", default-features = false, features = ["serde"] } 19 | deno_semver = "0.9.0" 20 | deno_error = "0.7.0" 21 | deno_lockfile = "0.32.0" 22 | indexmap = "2" 23 | monch = "0.5.0" 24 | log = "0.4" 25 | serde = { version = "1.0.130", features = ["derive", "rc"] } 26 | serde_json = { version = "1.0.67", features = ["preserve_order"] } 27 | thiserror = "2.0.3" 28 | futures = "0.3.28" 29 | url = "2" 30 | 31 | [dev-dependencies] 32 | divan = "0.1.17" 33 | hex = "0.4.3" 34 | pretty_assertions = "1.0.0" 35 | reqwest = "0.12.9" 36 | sha2 = "0.10.8" 37 | tokio = { version = "1.27.0", features = ["full"] } 38 | 39 | [profile.release-with-debug] 40 | inherits = "release" 41 | debug = true 42 | 43 | [[bench]] 44 | name = "bench" 45 | harness = false 46 | -------------------------------------------------------------------------------- /src/resolution/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. MIT license. 2 | 3 | mod collections; 4 | mod common; 5 | mod graph; 6 | mod snapshot; 7 | #[cfg(feature = "tracing")] 8 | mod tracing; 9 | 10 | pub use common::NewestDependencyDate; 11 | pub use common::NewestDependencyDateOptions; 12 | pub use common::NpmPackageVersionNotFound; 13 | pub use common::NpmPackageVersionResolutionError; 14 | pub use common::NpmPackageVersionResolver; 15 | pub use common::NpmVersionResolver; 16 | pub use graph::NpmResolutionError; 17 | pub use graph::Reporter; 18 | pub use graph::UnmetPeerDepDiagnostic; 19 | pub use snapshot::AddPkgReqsOptions; 20 | pub use snapshot::AddPkgReqsResult; 21 | pub use snapshot::DefaultTarballUrlProvider; 22 | pub use snapshot::IncompleteSnapshotFromLockfileError; 23 | pub use snapshot::NpmPackagesPartitioned; 24 | pub use snapshot::NpmRegistryDefaultTarballUrlProvider; 25 | pub use snapshot::NpmResolutionSnapshot; 26 | pub use snapshot::PackageCacheFolderIdNotFoundError; 27 | pub use snapshot::PackageIdNotFoundError; 28 | pub use snapshot::PackageNotFoundFromReferrerError; 29 | pub use snapshot::PackageNvNotFoundError; 30 | pub use snapshot::PackageReqNotFoundError; 31 | pub use snapshot::SerializedNpmResolutionSnapshot; 32 | pub use snapshot::SerializedNpmResolutionSnapshotPackage; 33 | pub use snapshot::SnapshotFromLockfileError; 34 | pub use snapshot::SnapshotFromLockfileParams; 35 | pub use snapshot::ValidSerializedNpmResolutionSnapshot; 36 | pub use snapshot::snapshot_from_lockfile; 37 | -------------------------------------------------------------------------------- /src/resolution/tracing/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use serde::Serialize; 4 | 5 | #[derive(Debug, Serialize)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct TraceGraphSnapshot { 8 | pub roots: BTreeMap, 9 | pub nodes: Vec, 10 | pub path: TraceGraphPath, 11 | } 12 | 13 | #[derive(Debug, Serialize)] 14 | #[serde(rename_all = "camelCase")] 15 | pub struct TraceNodeDependency { 16 | pub kind: String, 17 | pub bare_specifier: String, 18 | pub name: String, 19 | pub version_req: String, 20 | pub peer_dep_version_req: Option, 21 | } 22 | 23 | #[derive(Debug, Serialize)] 24 | #[serde(rename_all = "camelCase")] 25 | pub struct TraceNode { 26 | pub id: u32, 27 | pub resolved_id: String, 28 | pub children: BTreeMap, 29 | pub dependencies: Vec, 30 | } 31 | 32 | #[derive(Debug, Serialize)] 33 | #[serde(rename_all = "camelCase")] 34 | pub struct TraceGraphPath { 35 | pub specifier: String, 36 | pub node_id: u32, 37 | pub nv: String, 38 | pub previous: Option>, 39 | } 40 | 41 | pub fn output(traces: &[TraceGraphSnapshot]) { 42 | let json = serde_json::to_string(traces).unwrap(); 43 | let app_js = include_str!("./app.js"); 44 | let app_css = include_str!("./app.css"); 45 | let html = format!( 46 | " 47 | 50 |
51 |
52 | 53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | 67 | " 68 | ); 69 | let temp_file_path = std::env::temp_dir().join("deno-npm-trace.html"); 70 | std::fs::write(&temp_file_path, html).unwrap(); 71 | let url = format!( 72 | "file://{}", 73 | temp_file_path.to_string_lossy().replace('\\', "/") 74 | ); 75 | #[allow(clippy::print_stderr)] 76 | { 77 | eprintln!( 78 | "\n==============\nTrace output ready! Please open your browser to: {}\n==============\n", 79 | url 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/resolution/collections.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. MIT license. 2 | 3 | enum OneDirectionalLinkedListItem<'a, T> { 4 | Root, 5 | Item(&'a T), 6 | } 7 | 8 | /// A linked list that only points at the previously added item. 9 | pub struct OneDirectionalLinkedList<'a, T> { 10 | parent: Option<&'a OneDirectionalLinkedList<'a, T>>, 11 | value: OneDirectionalLinkedListItem<'a, T>, 12 | } 13 | 14 | impl Default for OneDirectionalLinkedList<'_, T> { 15 | fn default() -> Self { 16 | Self { 17 | parent: None, 18 | value: OneDirectionalLinkedListItem::Root, 19 | } 20 | } 21 | } 22 | 23 | impl<'a, T> OneDirectionalLinkedList<'a, T> { 24 | pub fn push(&'a self, item: &'a T) -> Self { 25 | Self { 26 | parent: Some(self), 27 | value: OneDirectionalLinkedListItem::Item(item), 28 | } 29 | } 30 | 31 | pub fn iter(&'a self) -> OneDirectionalLinkedListIterator<'a, T> { 32 | OneDirectionalLinkedListIterator { next: Some(self) } 33 | } 34 | } 35 | 36 | pub struct OneDirectionalLinkedListIterator<'a, T> { 37 | next: Option<&'a OneDirectionalLinkedList<'a, T>>, 38 | } 39 | 40 | impl<'a, T> Iterator for OneDirectionalLinkedListIterator<'a, T> { 41 | type Item = &'a T; 42 | 43 | fn next(&mut self) -> Option { 44 | match self.next.take() { 45 | Some(ancestor_id_node) => { 46 | self.next = ancestor_id_node.parent; 47 | match ancestor_id_node.value { 48 | OneDirectionalLinkedListItem::Root => None, 49 | OneDirectionalLinkedListItem::Item(id) => Some(id), 50 | } 51 | } 52 | None => None, 53 | } 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod test { 59 | use super::OneDirectionalLinkedList; 60 | 61 | #[test] 62 | fn test_linked_list() { 63 | let list = OneDirectionalLinkedList::default(); 64 | let item1 = list.push(&1); 65 | let item2 = item1.push(&2); 66 | let item3 = item2.push(&3); 67 | assert_eq!(item3.iter().copied().collect::>(), vec![3, 2, 1]); 68 | assert_eq!(item2.iter().copied().collect::>(), vec![2, 1]); 69 | assert_eq!(item1.iter().copied().collect::>(), vec![1]); 70 | assert_eq!( 71 | list.iter().copied().collect::>(), 72 | Vec::::new() 73 | ); 74 | assert_eq!(item3.iter().copied().collect::>(), vec![3, 2, 1]); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /benches/bench.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::HashMap; 3 | use std::rc::Rc; 4 | use std::sync::Arc; 5 | 6 | use deno_npm::registry::NpmPackageInfo; 7 | use deno_npm::registry::NpmRegistryApi; 8 | use deno_npm::registry::NpmRegistryPackageInfoLoadError; 9 | use deno_npm::registry::TestNpmRegistryApi; 10 | use deno_npm::resolution::AddPkgReqsOptions; 11 | use deno_npm::resolution::NpmResolutionSnapshot; 12 | use deno_npm::resolution::NpmVersionResolver; 13 | use deno_semver::package::PackageReq; 14 | use reqwest::StatusCode; 15 | 16 | fn main() { 17 | divan::main(); 18 | } 19 | 20 | mod deserialization { 21 | use super::*; 22 | 23 | #[divan::bench] 24 | fn packument(bencher: divan::Bencher) { 25 | let text = get_next_packument_text(); 26 | bencher.bench_local(|| { 27 | let value = serde_json::from_str::(&text).unwrap(); 28 | value.name.len() 29 | }); 30 | } 31 | 32 | #[divan::bench] 33 | fn packument_no_drop(bencher: divan::Bencher) { 34 | let text = get_next_packument_text(); 35 | bencher 36 | .bench_local(|| serde_json::from_str::(&text).unwrap()); 37 | } 38 | 39 | fn get_next_packument_text() -> String { 40 | build_rt().block_on(async { 41 | // ensure the fs cache is populated 42 | let _ = TargetFolderCachedRegistryApi::default() 43 | .package_info("next") 44 | .await 45 | .unwrap(); 46 | }); 47 | 48 | std::fs::read_to_string(packument_cache_filepath("next")).unwrap() 49 | } 50 | } 51 | 52 | mod resolution { 53 | use super::*; 54 | 55 | #[divan::bench] 56 | fn test(bencher: divan::Bencher) { 57 | let api = TestNpmRegistryApi::default(); 58 | let mut initial_pkgs = Vec::new(); 59 | const VERSION_COUNT: usize = 100; 60 | for pkg_index in 0..26 { 61 | let pkg_name = format!("a{}", pkg_index); 62 | let next_pkg = format!("a{}", pkg_index + 1); 63 | for version_index in 0..VERSION_COUNT { 64 | let version = format!("{}.0.0", version_index); 65 | if pkg_index == 0 { 66 | initial_pkgs.push(format!( 67 | "{}@{}", 68 | pkg_name.clone(), 69 | version.clone() 70 | )); 71 | } 72 | api.ensure_package_version(&pkg_name, &version); 73 | if pkg_index < 25 { 74 | api.add_dependency( 75 | (pkg_name.as_str(), version.as_str()), 76 | (next_pkg.as_str(), version.as_str()), 77 | ); 78 | } 79 | } 80 | } 81 | 82 | let rt = build_rt(); 83 | 84 | bencher.bench_local(|| { 85 | let snapshot = rt.block_on(async { 86 | run_resolver_and_get_snapshot(&api, &initial_pkgs).await 87 | }); 88 | 89 | assert_eq!(snapshot.top_level_packages().count(), VERSION_COUNT); 90 | }); 91 | } 92 | 93 | #[divan::bench(sample_count = 1000)] 94 | fn nextjs_resolve(bencher: divan::Bencher) { 95 | let api = TargetFolderCachedRegistryApi::default(); 96 | let rt = build_rt(); 97 | 98 | // run once to fill the caches 99 | rt.block_on(async { 100 | run_resolver_and_get_snapshot(&api, &["next@15.1.2".to_string()]).await 101 | }); 102 | 103 | bencher.bench_local(|| { 104 | let snapshot = rt.block_on(async { 105 | run_resolver_and_get_snapshot(&api, &["next@15.1.2".to_string()]).await 106 | }); 107 | 108 | assert_eq!(snapshot.top_level_packages().count(), 1); 109 | }); 110 | } 111 | } 112 | 113 | struct TargetFolderCachedRegistryApi { 114 | data: Rc>>>, 115 | } 116 | 117 | impl Default for TargetFolderCachedRegistryApi { 118 | fn default() -> Self { 119 | std::fs::create_dir_all("target/.deno_npm").unwrap(); 120 | Self { 121 | data: Default::default(), 122 | } 123 | } 124 | } 125 | 126 | #[async_trait::async_trait(?Send)] 127 | impl NpmRegistryApi for TargetFolderCachedRegistryApi { 128 | async fn package_info( 129 | &self, 130 | name: &str, 131 | ) -> Result, NpmRegistryPackageInfoLoadError> { 132 | if let Some(data) = self.data.borrow_mut().get(name).cloned() { 133 | return Ok(data); 134 | } 135 | let file_path = packument_cache_filepath(name); 136 | if let Ok(data) = std::fs::read_to_string(&file_path) 137 | && let Ok(data) = serde_json::from_str::>(&data) 138 | { 139 | self 140 | .data 141 | .borrow_mut() 142 | .insert(name.to_string(), data.clone()); 143 | return Ok(data); 144 | } 145 | let url = packument_url(name); 146 | eprintln!("Downloading {}", url); 147 | let resp = reqwest::get(&url).await.unwrap(); 148 | if resp.status() == StatusCode::NOT_FOUND { 149 | return Err(NpmRegistryPackageInfoLoadError::PackageNotExists { 150 | package_name: name.to_string(), 151 | }); 152 | } 153 | let text = resp.text().await.unwrap(); 154 | std::fs::write(&file_path, &text).unwrap(); 155 | let data = serde_json::from_str::>(&text).unwrap(); 156 | self 157 | .data 158 | .borrow_mut() 159 | .insert(name.to_string(), data.clone()); 160 | Ok(data) 161 | } 162 | } 163 | 164 | fn packument_cache_filepath(name: &str) -> String { 165 | format!("target/.deno_npm/{}", encode_package_name(name)) 166 | } 167 | 168 | fn packument_url(name: &str) -> String { 169 | format!("https://registry.npmjs.org/{}", encode_package_name(name)) 170 | } 171 | 172 | fn encode_package_name(name: &str) -> String { 173 | name.replace("/", "%2F") 174 | } 175 | 176 | fn build_rt() -> tokio::runtime::Runtime { 177 | tokio::runtime::Builder::new_current_thread() 178 | .enable_io() 179 | .enable_time() 180 | .build() 181 | .unwrap() 182 | } 183 | 184 | async fn run_resolver_and_get_snapshot( 185 | api: &impl NpmRegistryApi, 186 | reqs: &[String], 187 | ) -> NpmResolutionSnapshot { 188 | let snapshot = NpmResolutionSnapshot::new(Default::default()); 189 | let reqs = reqs 190 | .iter() 191 | .map(|req| PackageReq::from_str(req).unwrap()) 192 | .collect::>(); 193 | let version_resolver = NpmVersionResolver { 194 | types_node_version_req: None, 195 | link_packages: Default::default(), 196 | newest_dependency_date_options: Default::default(), 197 | }; 198 | let result = snapshot 199 | .add_pkg_reqs( 200 | api, 201 | AddPkgReqsOptions { 202 | package_reqs: &reqs, 203 | version_resolver: &version_resolver, 204 | should_dedup: true, 205 | }, 206 | None, 207 | ) 208 | .await; 209 | result.dep_graph_result.unwrap() 210 | } 211 | -------------------------------------------------------------------------------- /src/resolution/tracing/deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4", 3 | "specifiers": { 4 | "npm:@types/d3@7.4": "7.4.3" 5 | }, 6 | "npm": { 7 | "@types/d3-array@3.2.1": { 8 | "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" 9 | }, 10 | "@types/d3-axis@3.0.6": { 11 | "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", 12 | "dependencies": [ 13 | "@types/d3-selection" 14 | ] 15 | }, 16 | "@types/d3-brush@3.0.6": { 17 | "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", 18 | "dependencies": [ 19 | "@types/d3-selection" 20 | ] 21 | }, 22 | "@types/d3-chord@3.0.6": { 23 | "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==" 24 | }, 25 | "@types/d3-color@3.1.3": { 26 | "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" 27 | }, 28 | "@types/d3-contour@3.0.6": { 29 | "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", 30 | "dependencies": [ 31 | "@types/d3-array", 32 | "@types/geojson" 33 | ] 34 | }, 35 | "@types/d3-delaunay@6.0.4": { 36 | "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==" 37 | }, 38 | "@types/d3-dispatch@3.0.6": { 39 | "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==" 40 | }, 41 | "@types/d3-drag@3.0.7": { 42 | "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", 43 | "dependencies": [ 44 | "@types/d3-selection" 45 | ] 46 | }, 47 | "@types/d3-dsv@3.0.7": { 48 | "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==" 49 | }, 50 | "@types/d3-ease@3.0.2": { 51 | "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" 52 | }, 53 | "@types/d3-fetch@3.0.7": { 54 | "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", 55 | "dependencies": [ 56 | "@types/d3-dsv" 57 | ] 58 | }, 59 | "@types/d3-force@3.0.10": { 60 | "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==" 61 | }, 62 | "@types/d3-format@3.0.4": { 63 | "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==" 64 | }, 65 | "@types/d3-geo@3.1.0": { 66 | "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", 67 | "dependencies": [ 68 | "@types/geojson" 69 | ] 70 | }, 71 | "@types/d3-hierarchy@3.1.7": { 72 | "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==" 73 | }, 74 | "@types/d3-interpolate@3.0.4": { 75 | "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", 76 | "dependencies": [ 77 | "@types/d3-color" 78 | ] 79 | }, 80 | "@types/d3-path@3.1.1": { 81 | "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" 82 | }, 83 | "@types/d3-polygon@3.0.2": { 84 | "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==" 85 | }, 86 | "@types/d3-quadtree@3.0.6": { 87 | "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==" 88 | }, 89 | "@types/d3-random@3.0.3": { 90 | "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==" 91 | }, 92 | "@types/d3-scale-chromatic@3.1.0": { 93 | "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==" 94 | }, 95 | "@types/d3-scale@4.0.9": { 96 | "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", 97 | "dependencies": [ 98 | "@types/d3-time" 99 | ] 100 | }, 101 | "@types/d3-selection@3.0.11": { 102 | "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==" 103 | }, 104 | "@types/d3-shape@3.1.7": { 105 | "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", 106 | "dependencies": [ 107 | "@types/d3-path" 108 | ] 109 | }, 110 | "@types/d3-time-format@4.0.3": { 111 | "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==" 112 | }, 113 | "@types/d3-time@3.0.4": { 114 | "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" 115 | }, 116 | "@types/d3-timer@3.0.2": { 117 | "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" 118 | }, 119 | "@types/d3-transition@3.0.9": { 120 | "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", 121 | "dependencies": [ 122 | "@types/d3-selection" 123 | ] 124 | }, 125 | "@types/d3-zoom@3.0.8": { 126 | "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", 127 | "dependencies": [ 128 | "@types/d3-interpolate", 129 | "@types/d3-selection" 130 | ] 131 | }, 132 | "@types/d3@7.4.3": { 133 | "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", 134 | "dependencies": [ 135 | "@types/d3-array", 136 | "@types/d3-axis", 137 | "@types/d3-brush", 138 | "@types/d3-chord", 139 | "@types/d3-color", 140 | "@types/d3-contour", 141 | "@types/d3-delaunay", 142 | "@types/d3-dispatch", 143 | "@types/d3-drag", 144 | "@types/d3-dsv", 145 | "@types/d3-ease", 146 | "@types/d3-fetch", 147 | "@types/d3-force", 148 | "@types/d3-format", 149 | "@types/d3-geo", 150 | "@types/d3-hierarchy", 151 | "@types/d3-interpolate", 152 | "@types/d3-path", 153 | "@types/d3-polygon", 154 | "@types/d3-quadtree", 155 | "@types/d3-random", 156 | "@types/d3-scale", 157 | "@types/d3-scale-chromatic", 158 | "@types/d3-selection", 159 | "@types/d3-shape", 160 | "@types/d3-time", 161 | "@types/d3-time-format", 162 | "@types/d3-timer", 163 | "@types/d3-transition", 164 | "@types/d3-zoom" 165 | ] 166 | }, 167 | "@types/geojson@7946.0.16": { 168 | "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" 169 | } 170 | }, 171 | "remote": { 172 | "https://cdn.jsdelivr.net/npm/d3-array@3.2.4/+esm": "78417fa22c000897bb29a93e4349827420525ea050c1da59470ce0d2ffc38ed7", 173 | "https://cdn.jsdelivr.net/npm/d3-axis@3.0.0/+esm": "57d6bee70ce5e5ad5b9345fe082295373691bfbc09fb1a8383872ced5e8b75f5", 174 | "https://cdn.jsdelivr.net/npm/d3-brush@3.0.0/+esm": "21055d6256fd3059ef3488605115df7045d31c828f11a943438623a384398035", 175 | "https://cdn.jsdelivr.net/npm/d3-chord@3.0.1/+esm": "89f75f5f33bba0598b8d6f5101ae73cdd2f8b60fd75f2b8b42c72e6c993b294e", 176 | "https://cdn.jsdelivr.net/npm/d3-color@3.1.0/+esm": "1625144cfebcb716c65a701e75c6e37dcc85c42025abbf1f2653281e4c969585", 177 | "https://cdn.jsdelivr.net/npm/d3-contour@4.0.2/+esm": "71f30fddc7bc2989a48575dd29dffdd556d41dfd27e0aec3d62dd4c7752a547c", 178 | "https://cdn.jsdelivr.net/npm/d3-delaunay@6.0.4/+esm": "b275687c86e96e9b064bf794251f46ceb7c679d377df24e9e6a14d648f2c548e", 179 | "https://cdn.jsdelivr.net/npm/d3-dispatch@3.0.1/+esm": "e4695f8475fe76f53886e64aa39de56f198e17320bec9bf3fb00222d7b11b07d", 180 | "https://cdn.jsdelivr.net/npm/d3-drag@3.0.0/+esm": "26b57d06c8a7d59f20ad93184382e7f801d47ab0ad514e482ecf7b7456de0451", 181 | "https://cdn.jsdelivr.net/npm/d3-dsv@3.0.1/+esm": "54f382edf990ec040a204bf2b2eaa998377fe68e5b3b7ef7a0ef3e15194784b2", 182 | "https://cdn.jsdelivr.net/npm/d3-ease@3.0.1/+esm": "438ab7e4ec21fcdfe445aac5d275ce9ba8c087326b9859ba745ef3f35bd09a9b", 183 | "https://cdn.jsdelivr.net/npm/d3-fetch@3.0.1/+esm": "f8d208002a84498fb5573f3fa5bd324065168205e99b52834fe21c4610481d02", 184 | "https://cdn.jsdelivr.net/npm/d3-force@3.0.0/+esm": "f322efef93a6929de1eee62288113a4d900e6496ffef8a3deb609577fb94808e", 185 | "https://cdn.jsdelivr.net/npm/d3-format@3.1.0/+esm": "6b3a227445ae8347a704a554400976921cc6b7c897e0b3785fa11405e3bd7b26", 186 | "https://cdn.jsdelivr.net/npm/d3-geo@3.1.1/+esm": "cbfc4b7d04dd8f811d74bb65f57009fad18947a38074bbee1dfca7d0115deb59", 187 | "https://cdn.jsdelivr.net/npm/d3-hierarchy@3.1.2/+esm": "11823f8e7baee2c1e18a7fc3151c490df61b9c7a6f928a3ec7c3f092ed139c47", 188 | "https://cdn.jsdelivr.net/npm/d3-interpolate@3.0.1/+esm": "848b81558c926d0e8712cb752b9cfe01791759edfc3eafef2f356c2449663a30", 189 | "https://cdn.jsdelivr.net/npm/d3-path@3.1.0/+esm": "72b2d6e063733ce4f06f268c6a312a90a3c19371a3424f452d546ba786410b23", 190 | "https://cdn.jsdelivr.net/npm/d3-polygon@3.0.1/+esm": "3253d07bf9c45bd527a02ce9f00493495750bce5d84f5553d00abcc23ac6fc11", 191 | "https://cdn.jsdelivr.net/npm/d3-quadtree@3.0.1/+esm": "ca323394b5b8ec4d762187f6e58eb3c9b80bb387e683755a5f2c732af1c18465", 192 | "https://cdn.jsdelivr.net/npm/d3-random@3.0.1/+esm": "5e570579c3f551d8be78928c396903a2a6d6c418a27219906f081e7b1718ca4b", 193 | "https://cdn.jsdelivr.net/npm/d3-scale-chromatic@3.1.0/+esm": "32bbbaff73494ccc3e7c2474f29192fba3edd1672990620f394c042509997d0e", 194 | "https://cdn.jsdelivr.net/npm/d3-scale@4.0.2/+esm": "4587c00859188ea54fadcefd48eb7db0e9c86a825d0a2c53b87955cc3d5a53f2", 195 | "https://cdn.jsdelivr.net/npm/d3-selection@3.0.0/+esm": "d09694c85eb0d516da313bb64e7ed2fffd6af0350eaa81c013a6b46a7c261696", 196 | "https://cdn.jsdelivr.net/npm/d3-shape@3.2.0/+esm": "92e7bba7896b173b6993368440aaad468bfae22543d3edd0888494d82f2b581d", 197 | "https://cdn.jsdelivr.net/npm/d3-time-format@4.1.0/+esm": "c3df7a4b27574acb19ea11a47c43bf148872c61e8cc05f8068bf3d474be0bafb", 198 | "https://cdn.jsdelivr.net/npm/d3-time@3.1.0/+esm": "221c6c5bbf298aeb68c51b21829d7ba6d6fb2188a99fdb56f542db2ab7380874", 199 | "https://cdn.jsdelivr.net/npm/d3-timer@3.0.1/+esm": "aea11f14286cfb659fdebd6c18191ca0331f50fd70666d0e5fcf36086640b797", 200 | "https://cdn.jsdelivr.net/npm/d3-transition@3.0.1/+esm": "48b3c59260ca21e7e7365774dcdb4543eb8adaef5c3950185e19486c374019d9", 201 | "https://cdn.jsdelivr.net/npm/d3-zoom@3.0.0/+esm": "3e7f89db07ea19b000ae43861c48c2de7073c34b4a0f6f93f11cce9c2e352a9c", 202 | "https://cdn.jsdelivr.net/npm/d3@7/+esm": "1bf58b6f32b38975bb925da16063c526ef7c4a4f869503e27988f5bc4012e543", 203 | "https://cdn.jsdelivr.net/npm/delaunator@5.0.1/+esm": "831b2f2be969c0d85813d51302bae6194c0bf0ab8af10c3e2cdf2ca6556bc841", 204 | "https://cdn.jsdelivr.net/npm/internmap@2.0.3/+esm": "a2a6f711e62b7d68af42551c89ecc7895573d9d6e41ed5e31357bd7b7133d4a5", 205 | "https://cdn.jsdelivr.net/npm/robust-predicates@3.0.2/+esm": "2070874014e2379eabf1f0f37c5293b0f8b2a82c0f9cfe82f25e210a01cf060e" 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/npm_rc/ini.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. MIT license. 2 | 3 | // ini file parsing 4 | 5 | use std::borrow::Cow; 6 | 7 | use monch::*; 8 | 9 | #[derive(Debug, PartialEq, Eq)] 10 | pub enum KeyValueOrSection<'a> { 11 | KeyValue(KeyValue<'a>), 12 | Section(Section<'a>), 13 | } 14 | 15 | #[derive(Debug, PartialEq, Eq)] 16 | pub struct Section<'a> { 17 | pub header: &'a str, 18 | pub items: Vec>, 19 | } 20 | 21 | #[derive(Debug, PartialEq, Eq)] 22 | pub struct KeyValue<'a> { 23 | pub key: Key<'a>, 24 | pub value: Value<'a>, 25 | } 26 | 27 | #[derive(Debug, PartialEq, Eq)] 28 | pub enum Key<'a> { 29 | Plain(Cow<'a, str>), 30 | Array(Cow<'a, str>), 31 | } 32 | 33 | #[derive(Debug, PartialEq, Eq)] 34 | pub enum Value<'a> { 35 | String(Cow<'a, str>), 36 | Boolean(bool), 37 | Number(i64), 38 | Null, 39 | Undefined, 40 | } 41 | 42 | pub fn parse_ini( 43 | input: &str, 44 | ) -> Result>, ParseErrorFailureError> { 45 | with_failure_handling(|input| { 46 | let (input, _) = skip_trivia(input)?; 47 | let (input, items) = many0(|input| { 48 | let (input, kv_or_section) = parse_kv_or_section(input)?; 49 | let (input, _) = skip_trivia(input)?; 50 | Ok((input, kv_or_section)) 51 | })(input)?; 52 | Ok((input, items)) 53 | })(input) 54 | } 55 | 56 | fn parse_kv_or_section(input: &str) -> ParseResult<'_, KeyValueOrSection<'_>> { 57 | or( 58 | map(parse_section, KeyValueOrSection::Section), 59 | map(parse_key_value, KeyValueOrSection::KeyValue), 60 | )(input) 61 | } 62 | 63 | fn parse_section(input: &str) -> ParseResult<'_, Section<'_>> { 64 | let (input, _) = skip_non_newline_whitespace(input)?; 65 | let (input, header) = parse_section_header(input)?; 66 | let (input, _) = skip_non_newline_whitespace(input)?; 67 | let (input, _) = skip_trivia(input)?; 68 | let (input, items) = many0(|input| { 69 | let (input, kv) = parse_key_value(input)?; 70 | let (input, _) = skip_trivia(input)?; 71 | Ok((input, kv)) 72 | })(input)?; 73 | Ok((input, Section { header, items })) 74 | } 75 | 76 | fn parse_section_header(input: &str) -> ParseResult<'_, &str> { 77 | let (input, _) = ch('[')(input)?; 78 | let (input, header_text) = take_while(|c| c != ']' && c != '\n')(input)?; 79 | let (input, _) = ch(']')(input)?; 80 | 81 | Ok((input, header_text)) 82 | } 83 | 84 | fn parse_key_value(input: &str) -> ParseResult<'_, KeyValue<'_>> { 85 | fn parse_empty_value(input: &str) -> ParseResult<'_, ()> { 86 | let (input, _) = skip_non_newline_whitespace(input)?; 87 | let (input, _) = skip_comment(input)?; 88 | if input.is_empty() || input.starts_with('\n') || input.starts_with("\r\n") 89 | { 90 | Ok((input, ())) 91 | } else { 92 | Err(ParseError::Backtrace) 93 | } 94 | } 95 | 96 | let (input, key) = parse_key(input)?; 97 | let (input, _) = skip_non_newline_whitespace(input)?; 98 | let (input, value) = or( 99 | |input| { 100 | let (input, _) = ch('=')(input)?; 101 | parse_value(input) 102 | }, 103 | map(parse_empty_value, |_| Value::Boolean(true)), 104 | )(input)?; 105 | Ok((input, KeyValue { key, value })) 106 | } 107 | 108 | fn parse_key(input: &str) -> ParseResult<'_, Key<'_>> { 109 | fn parse_unquoted(input: &str) -> ParseResult<'_, Key<'_>> { 110 | let (input, key) = 111 | take_while_not_comment_and(|c| c != '=' && c != '\n')(input)?; 112 | let key = trim_cow_str(key); 113 | match strip_cow_str_suffix(&key, "[]") { 114 | Some(key) => Ok((input, Key::Array(key))), 115 | None => Ok((input, Key::Plain(key))), 116 | } 117 | } 118 | 119 | or( 120 | map(parse_quoted_skipping_spaces, Key::Plain), 121 | parse_unquoted, 122 | )(input) 123 | } 124 | 125 | fn parse_value(input: &str) -> ParseResult<'_, Value<'_>> { 126 | fn parse_unquoted(input: &str) -> ParseResult<'_, Value<'_>> { 127 | let (input, value) = take_until_comment_or_newline(input)?; 128 | let value = trim_cow_str(value); 129 | Ok(( 130 | input, 131 | match value.as_ref() { 132 | "true" => Value::Boolean(true), 133 | "false" => Value::Boolean(false), 134 | "null" => Value::Null, 135 | "undefined" => Value::Undefined, 136 | _ => { 137 | if let Ok(value) = value.parse::() { 138 | Value::Number(value) 139 | } else { 140 | Value::String(value) 141 | } 142 | } 143 | }, 144 | )) 145 | } 146 | 147 | or( 148 | map(parse_quoted_skipping_spaces, Value::String), 149 | parse_unquoted, 150 | )(input) 151 | } 152 | 153 | fn strip_cow_str_suffix<'a>( 154 | cow: &Cow<'a, str>, 155 | suffix: &str, 156 | ) -> Option> { 157 | match cow { 158 | Cow::Borrowed(s) => s.strip_suffix(suffix).map(Cow::Borrowed), 159 | Cow::Owned(s) => s 160 | .strip_suffix(suffix) 161 | .map(ToOwned::to_owned) 162 | .map(Cow::Owned), 163 | } 164 | } 165 | 166 | fn trim_cow_str(cow: Cow<'_, str>) -> Cow<'_, str> { 167 | match cow { 168 | Cow::Borrowed(s) => Cow::Borrowed(s.trim()), 169 | Cow::Owned(s) => Cow::Owned({ 170 | let trimmed = s.trim(); 171 | if trimmed.len() == s.len() { 172 | s // don't allocate 173 | } else { 174 | trimmed.to_string() 175 | } 176 | }), 177 | } 178 | } 179 | 180 | fn skip_trivia(input: &str) -> ParseResult<'_, ()> { 181 | let mut input = input; 182 | let mut length = 0; 183 | 184 | while input.len() != length { 185 | length = input.len(); 186 | input = skip_whitespace(input)?.0; 187 | input = skip_comment(input)?.0; 188 | } 189 | Ok((input, ())) 190 | } 191 | 192 | fn parse_quoted_skipping_spaces(input: &str) -> ParseResult<'_, Cow<'_, str>> { 193 | let (input, _) = skip_non_newline_whitespace(input)?; 194 | let (input, value) = parse_quoted_string(input)?; 195 | let (input, _) = skip_non_newline_whitespace(input)?; 196 | Ok((input, value)) 197 | } 198 | 199 | fn parse_quoted_string(input: &str) -> ParseResult<'_, Cow<'_, str>> { 200 | fn take_inner_text( 201 | quote_start_char: char, 202 | ) -> impl Fn(&str) -> ParseResult<'_, Cow> { 203 | move |input| { 204 | let mut last_char = None; 205 | let mut texts = Vec::new(); 206 | let mut start_index = 0; 207 | for (index, c) in input.char_indices() { 208 | if c == '\\' && last_char == Some('\\') { 209 | last_char = None; 210 | texts.push(&input[start_index..index - 1]); 211 | start_index = index; 212 | continue; 213 | } 214 | if c == quote_start_char { 215 | if last_char == Some('\\') { 216 | texts.push(&input[start_index..index - 1]); 217 | start_index = index; 218 | } else { 219 | texts.push(&input[start_index..index]); 220 | return Ok((&input[index..], { 221 | if texts.len() == 1 { 222 | Cow::Borrowed(texts[0]) 223 | } else { 224 | Cow::Owned(texts.concat()) 225 | } 226 | })); 227 | } 228 | } 229 | if c == '\n' { 230 | return Err(ParseError::Backtrace); 231 | } 232 | last_char = Some(c); 233 | } 234 | Err(ParseError::Backtrace) 235 | } 236 | } 237 | 238 | let (input, quote_start_char) = or(ch('"'), ch('\''))(input)?; 239 | let (input, quoted_text) = take_inner_text(quote_start_char)(input)?; 240 | let (input, _) = ch(quote_start_char)(input)?; 241 | Ok((input, quoted_text)) 242 | } 243 | 244 | fn skip_non_newline_whitespace(input: &str) -> ParseResult<'_, ()> { 245 | skip_while(|c| c == ' ' || c == '\t')(input) 246 | } 247 | 248 | fn skip_comment(input: &str) -> ParseResult<'_, ()> { 249 | let (input, maybe_found) = 250 | maybe(or(map(ch('#'), |_| ()), map(ch(';'), |_| ())))(input)?; 251 | if maybe_found.is_none() { 252 | return Ok((input, ())); 253 | } 254 | let (input, _) = skip_while(|c| c != '\n')(input)?; 255 | 256 | Ok((input, ())) 257 | } 258 | 259 | fn take_until_comment_or_newline(input: &str) -> ParseResult<'_, Cow<'_, str>> { 260 | take_while_not_comment_and(|c| c != '\n')(input) 261 | } 262 | 263 | fn take_while_not_comment_and<'a>( 264 | test: impl Fn(char) -> bool, 265 | ) -> impl Fn(&'a str) -> ParseResult<'a, Cow<'a, str>> { 266 | move |input| { 267 | let mut texts = Vec::new(); 268 | let mut last_char = None; 269 | let mut start_index = 0; 270 | let mut end_index = None; 271 | for (index, c) in input.char_indices() { 272 | if !test(c) { 273 | end_index = Some(index); 274 | break; 275 | } 276 | if c == '\\' && last_char == Some('\\') { 277 | texts.push(&input[start_index..index - 1]); 278 | start_index = index; 279 | last_char = None; 280 | continue; 281 | } 282 | if matches!(c, '#' | ';') { 283 | if last_char == Some('\\') { 284 | texts.push(&input[start_index..index - 1]); 285 | start_index = index; 286 | } else { 287 | end_index = Some(index); 288 | break; 289 | } 290 | } 291 | last_char = Some(c); 292 | } 293 | texts.push(&input[start_index..end_index.unwrap_or(input.len())]); 294 | Ok((&input[end_index.unwrap_or(input.len())..], { 295 | if texts.len() == 1 { 296 | Cow::Borrowed(texts[0]) 297 | } else { 298 | Cow::Owned(texts.concat()) 299 | } 300 | })) 301 | } 302 | } 303 | 304 | #[cfg(test)] 305 | mod test { 306 | use super::*; 307 | use pretty_assertions::assert_eq; 308 | 309 | #[test] 310 | fn parses_ini() { 311 | let ini = parse_ini( 312 | r#" 313 | a=1 314 | b="2" 315 | c = '3' 316 | d 317 | e = true;comment 318 | f = false # comment;test#;comment 319 | g = null 320 | h = undefined 321 | i[] = 1 322 | i[] = 2 323 | j = \;escaped\#not a comment\\#comment 324 | "k;#" = "a;#\"\\" 325 | 326 | [section] 327 | 328 | a = 1 329 | "#, 330 | ) 331 | .unwrap(); 332 | assert_eq!( 333 | ini, 334 | vec![ 335 | KeyValueOrSection::KeyValue(KeyValue { 336 | key: Key::Plain("a".into()), 337 | value: Value::Number(1), 338 | }), 339 | KeyValueOrSection::KeyValue(KeyValue { 340 | key: Key::Plain("b".into()), 341 | value: Value::String("2".into()), 342 | }), 343 | KeyValueOrSection::KeyValue(KeyValue { 344 | key: Key::Plain("c".into()), 345 | value: Value::String("3".into()), 346 | }), 347 | KeyValueOrSection::KeyValue(KeyValue { 348 | key: Key::Plain("d".into()), 349 | value: Value::Boolean(true) 350 | }), 351 | KeyValueOrSection::KeyValue(KeyValue { 352 | key: Key::Plain("e".into()), 353 | value: Value::Boolean(true), 354 | }), 355 | KeyValueOrSection::KeyValue(KeyValue { 356 | key: Key::Plain("f".into()), 357 | value: Value::Boolean(false), 358 | }), 359 | KeyValueOrSection::KeyValue(KeyValue { 360 | key: Key::Plain("g".into()), 361 | value: Value::Null, 362 | }), 363 | KeyValueOrSection::KeyValue(KeyValue { 364 | key: Key::Plain("h".into()), 365 | value: Value::Undefined, 366 | }), 367 | KeyValueOrSection::KeyValue(KeyValue { 368 | key: Key::Array("i".into()), 369 | value: Value::Number(1), 370 | }), 371 | KeyValueOrSection::KeyValue(KeyValue { 372 | key: Key::Array("i".into()), 373 | value: Value::Number(2), 374 | }), 375 | KeyValueOrSection::KeyValue(KeyValue { 376 | key: Key::Plain("j".into()), 377 | value: Value::String(";escaped#not a comment\\".into()), 378 | }), 379 | KeyValueOrSection::KeyValue(KeyValue { 380 | key: Key::Plain("k;#".into()), 381 | value: Value::String("a;#\"\\".into()), 382 | }), 383 | KeyValueOrSection::Section(Section { 384 | header: "section", 385 | items: vec![KeyValue { 386 | key: Key::Plain("a".into()), 387 | value: Value::Number(1), 388 | }] 389 | }), 390 | ] 391 | ) 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /src/resolution/tracing/app.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @typedef {Object} TraceGraphSnapshot 5 | * @property {Record} roots 6 | * @property {TraceNode[]} nodes 7 | * @property {TraceGraphPath} path 8 | */ 9 | 10 | /** 11 | * @typedef {Object} TraceNode 12 | * @property {number} id 13 | * @property {string} resolvedId 14 | * @property {Record} children 15 | * @property {TraceNodeDependency[]} dependencies 16 | */ 17 | 18 | /** 19 | * @typedef {Object} TraceGraphPath 20 | * @property {string} specifier 21 | * @property {number} nodeId 22 | * @property {string} nv 23 | * @property {?TraceGraphPath} previous 24 | */ 25 | 26 | /** 27 | * @typedef {Object} TraceNodeDependency 28 | * @property {string} kind 29 | * @property {string} bareSpecifier 30 | * @property {string} name 31 | * @property {string} versionReq 32 | * @property {string | undefined} peerDepVersionReq 33 | */ 34 | 35 | /** 36 | * @typedef {Object} GraphNode 37 | * @property {TraceNode} rawNode 38 | * @property {number} id 39 | * @property {GraphNode[]} sources 40 | * @property {GraphNode[]} targets 41 | * @property {number} x 42 | * @property {number} y 43 | */ 44 | 45 | // @ts-types="npm:@types/d3@7.4" 46 | import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm"; 47 | 48 | /** 49 | * @type {TraceGraphSnapshot[]} 50 | */ 51 | // @ts-ignore rawTraces is defined in the generated html file. 52 | const traces = rawTraces; 53 | /** @type {Map} */ 54 | const nodePositions = new Map(); 55 | const { nodeDepths, depthYChildCount } = analyzeTracesDepth(); 56 | /** @type {d3.ZoomTransform} */ 57 | let transform; 58 | const graphDiv = document.getElementById("graph"); 59 | const infoDiv = document.getElementById("info"); 60 | const stepTextDiv = document.getElementById("stepText"); 61 | /** @type {GraphNode[]} */ 62 | let nodes = undefined; 63 | initSlider(traces.length - 1, (index) => { 64 | refresh(index); 65 | }); 66 | 67 | refresh(0); 68 | 69 | /** @param {number} index */ 70 | function refresh(index) { 71 | const snapshot = traces[index]; 72 | const snapshotNodesMap = new Map(snapshot.nodes.map((n) => [n.id, n])); 73 | 74 | const svg = d3.select("#graph svg"); 75 | if (!svg.empty()) { 76 | // capture current zoom 77 | transform = d3.zoomTransform( 78 | /** @type {d3.ZoomedElementBaseType} */ (svg.node()), 79 | ); 80 | } 81 | if (nodes) { 82 | // Save current node positions 83 | nodes.forEach((node) => { 84 | nodePositions.set(node.id, { x: node.x, y: node.y }); 85 | }); 86 | } 87 | 88 | graphDiv.replaceChildren(); // remove the children 89 | 90 | stepTextDiv.textContent = `${index + 1}/${traces.length}`; 91 | setInfoNode(); 92 | createGraph(); 93 | 94 | function createGraph() { 95 | const result = getNodesAndLinks(); 96 | const { links, nodesMap } = result; 97 | nodes = result.nodes; 98 | const pathNodeIds = getPathNodeIds(); 99 | 100 | const width = graphDiv.clientWidth; 101 | const height = graphDiv.clientHeight; 102 | const svg = d3 103 | .select("#graph") 104 | .append("svg") 105 | .attr("viewBox", [0, 0, width, height]) 106 | .style("font", "40px sans-serif") 107 | .attr("width", width) 108 | .attr("height", height); 109 | 110 | const arrow = svg.append("svg:defs").selectAll("marker") 111 | .data(["end"]) 112 | .enter().append("svg:marker") 113 | .attr("id", String) 114 | .attr("orient", "auto"); 115 | const arrowInnerPath = arrow.append("svg:path").attr("fill", "#000"); 116 | 117 | const drag = d3 118 | .drag() 119 | .on("drag", function (event, d) { 120 | d.x = event.x; 121 | d.y = event.y; 122 | d3.select(this).raise().attr("transform", `translate(${d.x}, ${d.y})`); 123 | refreshLinks(); 124 | }); 125 | 126 | const nodeRadius = 15; 127 | const linkThickness = 5; 128 | const linkG = svg.append("g"); 129 | const link = linkG 130 | .selectAll("line") 131 | .data(links) 132 | .join("line") 133 | .attr("stroke-opacity", 0.6) 134 | .attr("stroke", (d) => { 135 | const bothOnPath = pathNodeIds.get(d.target) === d.source; 136 | return bothOnPath ? "red" : "black"; 137 | }) 138 | .style("stroke-width", linkThickness) 139 | .attr("marker-end", "url(#end)"); 140 | link.append("title") 141 | .text((d) => { 142 | return d.specifier; 143 | }); 144 | 145 | const nodeG = svg.append("g"); 146 | const nodeGInner = nodeG.append("g") 147 | .selectAll("g") 148 | .data(nodes) 149 | .join("g") 150 | .attr("transform", (d) => { 151 | return `translate(${d.x}, ${d.y})`; 152 | }).call(/** @type {any} */ (drag)); 153 | const nodeCircle = nodeGInner 154 | .append("circle") 155 | .attr("r", nodeRadius) 156 | .attr("fill", (d) => { 157 | const isGraphPath = pathNodeIds.has(d.id); 158 | return isGraphPath ? "red" : "blue"; 159 | }) 160 | .attr("stroke", "#000") 161 | .attr("id", (d) => `node${d.id}`) 162 | .on("click", (_, _d) => { 163 | // show more info on click in the future 164 | }); 165 | nodeGInner 166 | .append("text") 167 | .attr("x", 50) 168 | .attr("y", "0.31em") 169 | .text((d) => { 170 | return d.rawNode.resolvedId; 171 | }) 172 | .clone(true).lower() 173 | .attr("fill", "none") 174 | .attr("stroke", "white") 175 | .attr("stroke-width", 3); 176 | 177 | /** @type {number} */ 178 | let sqrtK; 179 | const zoom = d3.zoom().on("zoom", (e) => { 180 | applyTransform(e.transform); 181 | }); 182 | 183 | /** @param {d3.ZoomTransform} transform */ 184 | function applyTransform(transform) { 185 | nodeG.attr("transform", transform.toString()); 186 | sqrtK = Math.sqrt(transform.k); 187 | nodeCircle.attr("r", nodeRadius / sqrtK) 188 | .attr("stroke-width", 1 / sqrtK); 189 | 190 | linkG.attr("transform", transform.toString()); 191 | link.style("stroke-width", linkThickness / sqrtK); 192 | 193 | arrow.attr("markerWidth", 5) 194 | .attr("markerHeight", 5) 195 | .attr("viewBox", `0 0 ${5 / sqrtK} ${5 / sqrtK}`) 196 | .attr("refX", 8 / sqrtK) 197 | .attr("refY", 2.5 / sqrtK); 198 | arrowInnerPath.attr( 199 | "d", 200 | `M 0 0 L ${5 / sqrtK} ${2.5 / sqrtK} L 0 ${5 / sqrtK} z`, 201 | ); 202 | } 203 | 204 | svg.call(/** @type {any} */ (zoom)).call( 205 | /** @type {any} */ (zoom.transform), 206 | transform ?? d3.zoomIdentity, 207 | ); 208 | 209 | refreshLinks(); 210 | 211 | function refreshLinks() { 212 | link 213 | .attr("x1", (d) => nodesMap.get(d.source).x) 214 | .attr("y1", (d) => nodesMap.get(d.source).y) 215 | .attr("x2", (d) => nodesMap.get(d.target).x) 216 | .attr("y2", (d) => nodesMap.get(d.target).y); 217 | } 218 | } 219 | 220 | function getNodesAndLinks() { 221 | /** @param {number} id */ 222 | function getNodeY(id) { 223 | const nodeDepth = nodeDepths.get(id); 224 | let depthY = 0; 225 | for (let i = 0; i < nodeDepth.y; i++) { 226 | const childCount = depthYChildCount.get(i) ?? 1; 227 | depthY += childCount * 50; 228 | } 229 | const jitter = (Math.random() - 0.5) * 70; 230 | return depthY + nodeDepth.x * 200 + jitter; 231 | } 232 | 233 | /** @param {number} id */ 234 | function getNodeX(id) { 235 | const nodeDepth = nodeDepths.get(id); 236 | const center = width / 2; 237 | const childCount = depthYChildCount.get(nodeDepth.y) ?? 0; 238 | const jitter = (Math.random() - 0.5) * 70; 239 | return center + (nodeDepth.x - (childCount / 2)) * 255 + jitter; 240 | } 241 | 242 | const width = graphDiv.clientWidth; 243 | /** @type {GraphNode[]} */ 244 | const nodes = []; 245 | /** @type {Set} */ 246 | const seen = new Set(); 247 | const pendingNodes = Object.values(snapshot.roots); 248 | while (pendingNodes.length > 0) { 249 | const id = pendingNodes.shift(); 250 | if (seen.has(id)) { 251 | continue; 252 | } 253 | seen.add(id); 254 | const savedPosition = nodePositions.get(id); 255 | const node = snapshotNodesMap.get(id); 256 | nodes.push({ 257 | id: node.id, 258 | rawNode: node, 259 | sources: /** @type {GraphNode[]} */ ([]), 260 | targets: /** @type {GraphNode[]} */ ([]), 261 | x: savedPosition?.x ?? getNodeX(node.id), 262 | y: savedPosition?.y ?? getNodeY(node.id), 263 | }); 264 | pendingNodes.push(...Object.values(node.children)); 265 | } 266 | const nodesMap = new Map(nodes.map((n) => [n.rawNode.id, n])); 267 | /** @type {{ source: number; target: number; specifier: string; }[]} */ 268 | const links = []; 269 | 270 | for (const node of nodes) { 271 | const rawNode = node.rawNode; 272 | for (const [specifier, child] of Object.entries(rawNode.children)) { 273 | addLink(specifier, node, getNodeById(child)); 274 | } 275 | } 276 | 277 | return { nodes, nodesMap, links }; 278 | 279 | /** 280 | * @param {string} specifier 281 | * @param {GraphNode} source 282 | * @param {GraphNode} target 283 | */ 284 | function addLink(specifier, source, target) { 285 | source.targets.push(target); 286 | target.sources.push(source); 287 | links.push({ 288 | specifier, 289 | source: source.id, 290 | target: target.id, 291 | }); 292 | } 293 | 294 | /** @param {number} id */ 295 | function getNodeById(id) { 296 | const node = nodesMap.get(id); 297 | if (node == null) { 298 | throw new Error(`Could not find node: ${id}`); 299 | } 300 | return node; 301 | } 302 | } 303 | 304 | function getPathNodeIds() { 305 | let currentPath = snapshot.path; 306 | /** @type {Map} */ 307 | const nodes = new Map(); 308 | while (currentPath != null) { 309 | nodes.set(currentPath.nodeId, currentPath.previous?.nodeId); 310 | currentPath = currentPath.previous; 311 | } 312 | return nodes; 313 | } 314 | 315 | function setInfoNode() { 316 | let currentPath = snapshot.path; 317 | infoDiv.replaceChildren(); // clear 318 | while (currentPath != null) { 319 | const currentNode = snapshotNodesMap.get(currentPath.nodeId); 320 | infoDiv.appendChild(getRawNodeDiv(currentNode)); 321 | currentPath = currentPath.previous; 322 | } 323 | } 324 | } 325 | 326 | /** 327 | * @param {number} max 328 | * @param {(value: number) => void} onChange */ 329 | function initSlider(max, onChange) { 330 | /** @type {HTMLInputElement} */ 331 | const input = document.querySelector("#slider input"); 332 | input.min = "0"; 333 | input.max = max.toString(); 334 | input.addEventListener("input", () => { 335 | onChange(input.valueAsNumber); 336 | }); 337 | input.value = "0"; 338 | } 339 | 340 | /** @param {TraceNode} rawNode */ 341 | function getRawNodeDiv(rawNode) { 342 | const div = document.createElement("div"); 343 | const title = document.createElement("h3"); 344 | title.textContent = `${rawNode.resolvedId} (${rawNode.id})`; 345 | div.appendChild(title); 346 | const ul = document.createElement("ul"); 347 | for (const dep of rawNode.dependencies) { 348 | const li = document.createElement("li"); 349 | let text = `${dep.kind} - ${dep.bareSpecifier} - ${dep.versionReq}`; 350 | if (dep.peerDepVersionReq != null) { 351 | text += ` - ${dep.peerDepVersionReq}`; 352 | } 353 | li.textContent = text; 354 | ul.appendChild(li); 355 | } 356 | div.appendChild(ul); 357 | return div; 358 | } 359 | 360 | function analyzeTracesDepth() { 361 | /** @type {Map} */ 362 | const nodeDepths = new Map(); 363 | /** @type {Map} */ 364 | const depthYChildCount = new Map(); 365 | /** @type {Map} */ 366 | let nodesMap = new Map(); 367 | /** @type {Set} */ 368 | const seenNodes = new Set(); 369 | 370 | for (const snapshot of traces) { 371 | seenNodes.clear(); 372 | nodesMap = new Map(snapshot.nodes.map((n) => [n.id, n])); 373 | setDepthY( 374 | Object.values(snapshot.roots).map((start) => nodesMap.get(start)), 375 | ); 376 | } 377 | 378 | // certain nodes might be disconnected... add those here 379 | for (const snapshot of traces) { 380 | setDepthY(snapshot.nodes.filter((n) => !nodeDepths.has(n.id))); 381 | } 382 | 383 | return { 384 | nodeDepths, 385 | depthYChildCount, 386 | }; 387 | 388 | /** @param {TraceNode[]} firstNodes */ 389 | function setDepthY(firstNodes) { 390 | /** @type {[TraceNode, number][]} */ 391 | const nodesToAnalyze = firstNodes.map((node) => [node, 0]); 392 | 393 | while (nodesToAnalyze.length > 0) { 394 | const next = nodesToAnalyze.shift(); 395 | const [node, depth] = next; 396 | if (seenNodes.has(node.id)) { 397 | continue; 398 | } 399 | seenNodes.add(node.id); 400 | if (!nodeDepths.has(node.id)) { 401 | const childIndex = depthYChildCount.get(depth) ?? 0; 402 | nodeDepths.set(node.id, { 403 | y: depth, 404 | x: childIndex, 405 | }); 406 | depthYChildCount.set(depth, childIndex + 1); 407 | } 408 | for (const child of Object.values(node.children)) { 409 | nodesToAnalyze.push([nodesMap.get(child), depth + 1]); 410 | } 411 | } 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /examples/min_repro_solver.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::BTreeSet; 3 | use std::collections::HashMap; 4 | use std::path::PathBuf; 5 | use std::sync::Arc; 6 | 7 | use deno_npm::registry::NpmPackageInfo; 8 | use deno_npm::registry::NpmPackageVersionInfo; 9 | use deno_npm::registry::NpmRegistryApi; 10 | use deno_npm::registry::NpmRegistryPackageInfoLoadError; 11 | use deno_npm::resolution::AddPkgReqsOptions; 12 | use deno_npm::resolution::NpmResolutionSnapshot; 13 | use deno_npm::resolution::NpmVersionResolver; 14 | use deno_semver::package::PackageNv; 15 | use deno_semver::package::PackageReq; 16 | use reqwest::StatusCode; 17 | 18 | /// This example is not an example, but is a tool to create a minimal 19 | /// reproduction of a bug from a set of real npm package requirements 20 | /// and a provided condition. 21 | /// 22 | /// 1. Provide your package requirements below. 23 | /// 2. Update the condition saying what the bug is. 24 | /// 3. Run `cargo run --example min_repro_solver` 25 | /// 26 | /// This will output some test code that you can use in order to have 27 | /// a small reproduction of the bug. 28 | /// 29 | /// Additionally, it will populate the `.bench-reg` folder which allows 30 | /// running a custom npm registry locally via: 31 | /// 32 | /// ```sh 33 | /// deno run --allow-net --allow-read=. --allow-write=. jsr:@david/bench-registry@0.3.2/cli --cached-only 34 | /// ``` 35 | /// 36 | /// Then do `deno clean ; pnpm cache delete`, create an `.npmrc` with: 37 | /// 38 | /// ```ini 39 | /// registry=http://localhost:8000/npm/ 40 | /// ``` 41 | /// 42 | /// ...then a package.json with the package requirements below, and you can 43 | /// compare the reproduction in other package managers. 44 | #[tokio::main(flavor = "current_thread")] 45 | async fn main() { 46 | let mut solver = 47 | MinimalReproductionSolver::new(&["@aws-cdk/aws-ecs"], |snapshot| { 48 | snapshot.all_packages_for_every_system().any(|s| { 49 | s.id.as_serialized().chars().filter(|c| *c == '_').count() > 200 50 | }) 51 | }) 52 | .await; 53 | let mut had_change = true; 54 | while had_change { 55 | had_change = false; 56 | had_change |= solver.attempt_reduce_reqs().await; 57 | had_change |= solver.attempt_reduce_dependendencies().await; 58 | } 59 | 60 | eprintln!("==========================="); 61 | eprintln!("Test code"); 62 | eprintln!("==========================="); 63 | let test_code = solver.get_test_code(); 64 | println!("{}", test_code); 65 | solver.output_bench_registry_folder(); 66 | } 67 | 68 | struct MinimalReproductionSolver { 69 | reqs: Vec, 70 | condition: Box bool + 'static>, 71 | api: SubsetRegistryApi, 72 | current_snapshot: NpmResolutionSnapshot, 73 | } 74 | 75 | impl MinimalReproductionSolver { 76 | pub async fn new( 77 | reqs: &[&str], 78 | condition: impl Fn(&NpmResolutionSnapshot) -> bool + 'static, 79 | ) -> Self { 80 | let reqs = reqs.iter().map(|r| r.to_string()).collect::>(); 81 | let api = SubsetRegistryApi::default(); 82 | let snapshot = run_resolver_and_get_snapshot(&api, &reqs).await; 83 | assert!(condition(&snapshot), "bug does not exist in provided setup"); 84 | MinimalReproductionSolver { 85 | reqs, 86 | condition: Box::new(condition), 87 | api, 88 | current_snapshot: snapshot, 89 | } 90 | } 91 | 92 | pub async fn attempt_reduce_reqs(&mut self) -> bool { 93 | let mut made_reduction = false; 94 | for i in (0..self.reqs.len()).rev() { 95 | if self.reqs.len() <= 1 { 96 | break; 97 | } 98 | let mut new_reqs = self.reqs.clone(); 99 | let removed_req = new_reqs.remove(i); 100 | eprintln!("Checking removal of package req {}", removed_req); 101 | let changed = self 102 | .resolve_and_update_state_if_matches_condition(None, Some(new_reqs)) 103 | .await; 104 | if changed { 105 | made_reduction = true; 106 | eprintln!("Removed req: {}", removed_req); 107 | } 108 | } 109 | made_reduction 110 | } 111 | 112 | pub async fn attempt_reduce_dependendencies(&mut self) -> bool { 113 | let mut made_reduction = false; 114 | let package_nvs = self.current_nvs(); 115 | 116 | for package_nv in package_nvs { 117 | let dep_names = { 118 | let version_info = self.api.get_version_info(&package_nv); 119 | version_info 120 | .dependencies 121 | .keys() 122 | .chain(version_info.optional_dependencies.keys()) 123 | .chain(version_info.peer_dependencies.keys()) 124 | .cloned() 125 | .collect::>() 126 | }; 127 | for dep_name in dep_names { 128 | eprintln!("{}: checking removal of {}", package_nv, dep_name); 129 | let new_api = self.api.clone(); 130 | let mut new_version_info = 131 | self.api.get_version_info(&package_nv).clone(); 132 | new_version_info.dependencies.remove(&dep_name); 133 | new_version_info.optional_dependencies.remove(&dep_name); 134 | new_version_info.peer_dependencies.remove(&dep_name); 135 | new_version_info.peer_dependencies_meta.remove(&dep_name); 136 | new_api.set_package_version_info(&package_nv, new_version_info); 137 | let changed = self 138 | .resolve_and_update_state_if_matches_condition(Some(new_api), None) 139 | .await; 140 | if changed { 141 | made_reduction = true; 142 | eprintln!("{}: removed {}", package_nv, dep_name); 143 | } 144 | } 145 | } 146 | 147 | made_reduction 148 | } 149 | 150 | async fn resolve_and_update_state_if_matches_condition( 151 | &mut self, 152 | api: Option, 153 | reqs: Option>, 154 | ) -> bool { 155 | let snapshot = run_resolver_and_get_snapshot( 156 | api.as_ref().unwrap_or(&self.api), 157 | reqs.as_ref().unwrap_or(&self.reqs), 158 | ) 159 | .await; 160 | if !(self.condition)(&snapshot) { 161 | return false; 162 | } 163 | if let Some(api) = api { 164 | self.api = api; 165 | } 166 | if let Some(reqs) = reqs { 167 | self.reqs = reqs; 168 | } 169 | true 170 | } 171 | 172 | fn current_nvs(&self) -> BTreeSet { 173 | self 174 | .current_snapshot 175 | .all_packages_for_every_system() 176 | .map(|pkg| pkg.id.nv.clone()) 177 | .collect::>() 178 | } 179 | 180 | fn get_test_code(&self) -> String { 181 | let mut text = String::new(); 182 | 183 | text.push_str("let api = TestNpmRegistryApi::default();\n"); 184 | 185 | let nvs = self.current_nvs(); 186 | 187 | for nv in &nvs { 188 | text.push_str(&format!( 189 | "api.ensure_package_version(\"{}\", \"{}\");\n", 190 | nv.name, nv.version 191 | )); 192 | } 193 | 194 | for nv in &nvs { 195 | if !text.ends_with("\n\n") { 196 | text.push('\n'); 197 | } 198 | // text.push_str(&format!("// {}\n", nv)); 199 | let version_info = self.api.get_version_info(nv); 200 | for (key, value) in &version_info.dependencies { 201 | text.push_str(&format!( 202 | "api.add_dependency((\"{}\", \"{}\"), (\"{}\", \"{}\"));\n", 203 | nv.name, nv.version, key, value 204 | )); 205 | } 206 | for (key, value) in &version_info.peer_dependencies { 207 | let is_optional = version_info 208 | .peer_dependencies_meta 209 | .get(key) 210 | .map(|m| m.optional) 211 | .unwrap_or(false); 212 | if is_optional { 213 | text.push_str(&format!( 214 | "api.add_optional_peer_dependency((\"{}\", \"{}\"), (\"{}\", \"{}\"));\n", 215 | nv.name, nv.version, key, value 216 | )); 217 | } else { 218 | text.push_str(&format!( 219 | "api.add_peer_dependency((\"{}\", \"{}\"), (\"{}\", \"{}\"));\n", 220 | nv.name, nv.version, key, value 221 | )); 222 | } 223 | } 224 | } 225 | 226 | let reqs = self 227 | .reqs 228 | .iter() 229 | .map(|k| format!("\"{}\"", k)) 230 | .collect::>(); 231 | text.push_str( 232 | "\nlet (packages, package_reqs) = run_resolver_and_get_output(\n", 233 | ); 234 | text.push_str(" api,\n"); 235 | text.push_str(" vec!["); 236 | text.push_str(&reqs.join(", ")); 237 | text.push_str("],\n"); 238 | text.push_str(").await;\n"); 239 | text.push_str("assert_eq!(packages, vec![]);\n"); 240 | text.push_str("assert_eq!(package_reqs, vec![]);\n"); 241 | 242 | text 243 | } 244 | 245 | /// Output a .bench-reg folder so that the output can be compared 246 | /// with other package managers when using: https://jsr.io/@david/bench-registry@0.2.0 247 | fn output_bench_registry_folder(&self) { 248 | let package_infos = self.api.get_all_package_infos(); 249 | let nvs = self.current_nvs(); 250 | let bench_reg_folder = PathBuf::from(".bench-reg"); 251 | std::fs::create_dir_all(&bench_reg_folder).unwrap(); 252 | // trim down the package infos to only contain the found nvs 253 | for mut package_info in package_infos { 254 | let keys_to_remove = package_info 255 | .versions 256 | .keys() 257 | .filter(|&v| { 258 | let nv = PackageNv { 259 | name: package_info.name.clone(), 260 | version: (v).clone(), 261 | }; 262 | !nvs.contains(&nv) 263 | }) 264 | .cloned() 265 | .collect::>(); 266 | for key in &keys_to_remove { 267 | package_info.versions.remove(key); 268 | } 269 | let url = packument_url(&package_info.name); 270 | let file_path = bench_reg_folder.join(sha256_hex(&url)); 271 | std::fs::write( 272 | &file_path, 273 | serde_json::to_string_pretty(&package_info).unwrap(), 274 | ) 275 | .unwrap(); 276 | std::fs::write(file_path.with_extension("headers"), "{}").unwrap(); 277 | } 278 | } 279 | } 280 | 281 | async fn run_resolver_and_get_snapshot( 282 | api: &SubsetRegistryApi, 283 | reqs: &[String], 284 | ) -> NpmResolutionSnapshot { 285 | let snapshot = NpmResolutionSnapshot::new(Default::default()); 286 | let reqs = reqs 287 | .iter() 288 | .map(|req| PackageReq::from_str(req).unwrap()) 289 | .collect::>(); 290 | let version_resolver = NpmVersionResolver { 291 | types_node_version_req: None, 292 | link_packages: Default::default(), 293 | newest_dependency_date_options: Default::default(), 294 | }; 295 | let result = snapshot 296 | .add_pkg_reqs( 297 | api, 298 | AddPkgReqsOptions { 299 | package_reqs: &reqs, 300 | version_resolver: &version_resolver, 301 | should_dedup: true, 302 | }, 303 | None, 304 | ) 305 | .await; 306 | result.dep_graph_result.unwrap() 307 | } 308 | 309 | #[derive(Clone)] 310 | struct SubsetRegistryApi { 311 | data: RefCell>>, 312 | } 313 | 314 | impl Default for SubsetRegistryApi { 315 | fn default() -> Self { 316 | std::fs::create_dir_all("target/.deno_npm").unwrap(); 317 | Self { 318 | data: Default::default(), 319 | } 320 | } 321 | } 322 | 323 | impl SubsetRegistryApi { 324 | pub fn get_all_package_infos(&self) -> Vec { 325 | self 326 | .data 327 | .borrow() 328 | .values() 329 | .map(|i| i.as_ref().clone()) 330 | .collect() 331 | } 332 | 333 | pub fn get_version_info(&self, nv: &PackageNv) -> NpmPackageVersionInfo { 334 | self 335 | .data 336 | .borrow() 337 | .get(nv.name.as_str()) 338 | .unwrap() 339 | .versions 340 | .get(&nv.version) 341 | .unwrap() 342 | .clone() 343 | } 344 | 345 | pub fn set_package_version_info( 346 | &self, 347 | nv: &PackageNv, 348 | version_info: NpmPackageVersionInfo, 349 | ) { 350 | let mut data = self.data.borrow_mut(); 351 | let mut package_info = data.get(nv.name.as_str()).unwrap().as_ref().clone(); 352 | package_info 353 | .versions 354 | .insert(nv.version.clone(), version_info); 355 | data.insert(nv.name.to_string(), Arc::new(package_info)); 356 | } 357 | } 358 | 359 | #[async_trait::async_trait(?Send)] 360 | impl NpmRegistryApi for SubsetRegistryApi { 361 | async fn package_info( 362 | &self, 363 | name: &str, 364 | ) -> Result, NpmRegistryPackageInfoLoadError> { 365 | if let Some(data) = self.data.borrow_mut().get(name).cloned() { 366 | return Ok(data); 367 | } 368 | let file_path = PathBuf::from(packument_cache_filepath(name)); 369 | if let Ok(data) = std::fs::read_to_string(&file_path) 370 | && let Ok(data) = serde_json::from_str::>(&data) 371 | { 372 | self 373 | .data 374 | .borrow_mut() 375 | .insert(name.to_string(), data.clone()); 376 | return Ok(data); 377 | } 378 | let url = packument_url(name); 379 | eprintln!("Downloading {}", url); 380 | let resp = reqwest::get(&url).await.unwrap(); 381 | if resp.status() == StatusCode::NOT_FOUND { 382 | return Err(NpmRegistryPackageInfoLoadError::PackageNotExists { 383 | package_name: name.to_string(), 384 | }); 385 | } 386 | let text = resp.text().await.unwrap(); 387 | let temp_path = file_path.with_extension(".tmp"); 388 | std::fs::write(&temp_path, &text).unwrap(); 389 | std::fs::rename(&temp_path, &file_path).unwrap(); 390 | let data = serde_json::from_str::>(&text).unwrap(); 391 | self 392 | .data 393 | .borrow_mut() 394 | .insert(name.to_string(), data.clone()); 395 | Ok(data) 396 | } 397 | } 398 | 399 | fn packument_cache_filepath(name: &str) -> String { 400 | format!("target/.deno_npm/{}", encode_package_name(name)) 401 | } 402 | 403 | fn packument_url(name: &str) -> String { 404 | format!("https://registry.npmjs.org/{}", encode_package_name(name)) 405 | } 406 | 407 | fn encode_package_name(name: &str) -> String { 408 | name.replace("/", "%2f") 409 | } 410 | 411 | fn sha256_hex(input: &str) -> String { 412 | use hex; 413 | use sha2::Digest; 414 | use sha2::Sha256; 415 | 416 | let mut hasher = Sha256::new(); 417 | hasher.update(input.as_bytes()); 418 | let result = hasher.finalize(); 419 | hex::encode(result) 420 | } 421 | -------------------------------------------------------------------------------- /src/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 | 7 | use std::cmp::Ordering; 8 | use std::collections::BTreeMap; 9 | use std::collections::HashMap; 10 | use std::collections::HashSet; 11 | 12 | use capacity_builder::CapacityDisplay; 13 | use capacity_builder::StringAppendable; 14 | use capacity_builder::StringBuilder; 15 | use deno_error::JsError; 16 | use deno_semver::CowVec; 17 | use deno_semver::SmallStackString; 18 | use deno_semver::StackString; 19 | use deno_semver::Version; 20 | use deno_semver::package::PackageNv; 21 | use registry::NpmPackageVersionBinEntry; 22 | use registry::NpmPackageVersionDistInfo; 23 | use resolution::SerializedNpmResolutionSnapshotPackage; 24 | use serde::Deserialize; 25 | use serde::Serialize; 26 | use thiserror::Error; 27 | 28 | pub mod npm_rc; 29 | pub mod registry; 30 | pub mod resolution; 31 | 32 | #[derive(Debug, Error, Clone, JsError)] 33 | #[class(type)] 34 | #[error("Invalid npm package id '{text}'. {message}")] 35 | pub 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 | )] 52 | pub struct NpmPackageIdPeerDependencies(CowVec); 53 | 54 | impl From<[NpmPackageId; N]> for NpmPackageIdPeerDependencies { 55 | fn from(value: [NpmPackageId; N]) -> Self { 56 | Self(CowVec::from(value)) 57 | } 58 | } 59 | 60 | impl 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 { 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 | 94 | impl<'a> StringAppendable<'a> for &'a NpmPackageIdPeerDependencies { 95 | fn append_to_builder( 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 | )] 108 | pub struct NpmPackageId { 109 | pub nv: PackageNv, 110 | pub peer_dependencies: NpmPackageIdPeerDependencies, 111 | } 112 | 113 | // Custom debug implementation for more concise test output 114 | impl 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 | 120 | impl 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 { 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> { 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 | 243 | impl<'a> capacity_builder::StringAppendable<'a> for &'a NpmPackageId { 244 | fn append_to_builder( 245 | self, 246 | builder: &mut capacity_builder::StringBuilder<'a, TString>, 247 | ) { 248 | self.as_serialized_with_level(builder, 0) 249 | } 250 | } 251 | 252 | impl 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 | 261 | impl PartialOrd for NpmPackageId { 262 | fn partial_cmp(&self, other: &Self) -> Option { 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)] 270 | pub 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 | 277 | impl NpmPackageCacheFolderId { 278 | pub fn with_no_count(&self) -> Self { 279 | Self { 280 | nv: self.nv.clone(), 281 | copy_index: 0, 282 | } 283 | } 284 | } 285 | 286 | impl 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)] 297 | pub struct NpmResolutionPackageSystemInfo { 298 | pub os: Vec, 299 | pub cpu: Vec, 300 | } 301 | 302 | impl 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)] 317 | pub 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 | /// The information used for installing the package. When `None`, 327 | /// it means the package was a workspace linked package and 328 | /// the local copy should be used instead. 329 | pub dist: Option, 330 | /// Key is what the package refers to the other package as, 331 | /// which could be different from the package name. 332 | pub dependencies: HashMap, 333 | pub optional_dependencies: HashSet, 334 | pub optional_peer_dependencies: HashSet, 335 | #[serde(flatten)] 336 | pub extra: Option, 337 | #[serde(skip)] 338 | pub is_deprecated: bool, 339 | #[serde(skip)] 340 | pub has_bin: bool, 341 | #[serde(skip)] 342 | pub has_scripts: bool, 343 | } 344 | 345 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] 346 | #[serde(rename_all = "camelCase", default)] 347 | pub struct NpmPackageExtraInfo { 348 | pub bin: Option, 349 | pub scripts: HashMap, 350 | pub deprecated: Option, 351 | } 352 | 353 | impl std::fmt::Debug for NpmResolutionPackage { 354 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 355 | // custom debug implementation for deterministic output in the tests 356 | f.debug_struct("NpmResolutionPackage") 357 | .field("pkg_id", &self.id) 358 | .field("copy_index", &self.copy_index) 359 | .field("system", &self.system) 360 | .field("extra", &self.extra) 361 | .field("is_deprecated", &self.is_deprecated) 362 | .field("has_bin", &self.has_bin) 363 | .field("has_scripts", &self.has_scripts) 364 | .field( 365 | "dependencies", 366 | &self.dependencies.iter().collect::>(), 367 | ) 368 | .field("optional_dependencies", &{ 369 | let mut deps = self.optional_dependencies.iter().collect::>(); 370 | deps.sort(); 371 | deps 372 | }) 373 | .field("dist", &self.dist) 374 | .finish() 375 | } 376 | } 377 | 378 | impl NpmResolutionPackage { 379 | pub fn as_serialized(&self) -> SerializedNpmResolutionSnapshotPackage { 380 | SerializedNpmResolutionSnapshotPackage { 381 | id: self.id.clone(), 382 | system: self.system.clone(), 383 | dependencies: self.dependencies.clone(), 384 | optional_peer_dependencies: self.optional_peer_dependencies.clone(), 385 | optional_dependencies: self.optional_dependencies.clone(), 386 | dist: self.dist.clone(), 387 | extra: self.extra.clone(), 388 | is_deprecated: self.is_deprecated, 389 | has_bin: self.has_bin, 390 | has_scripts: self.has_scripts, 391 | } 392 | } 393 | 394 | pub fn get_package_cache_folder_id(&self) -> NpmPackageCacheFolderId { 395 | NpmPackageCacheFolderId { 396 | nv: self.id.nv.clone(), 397 | copy_index: self.copy_index, 398 | } 399 | } 400 | } 401 | 402 | /// System information used to determine which optional packages 403 | /// to download. 404 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 405 | pub struct NpmSystemInfo { 406 | /// `process.platform` value from Node.js 407 | pub os: SmallStackString, 408 | /// `process.arch` value from Node.js 409 | pub cpu: SmallStackString, 410 | } 411 | 412 | impl Default for NpmSystemInfo { 413 | fn default() -> Self { 414 | Self { 415 | os: node_js_os(std::env::consts::OS).into(), 416 | cpu: node_js_cpu(std::env::consts::ARCH).into(), 417 | } 418 | } 419 | } 420 | 421 | impl NpmSystemInfo { 422 | pub fn from_rust(os: &str, cpu: &str) -> Self { 423 | Self { 424 | os: node_js_os(os).into(), 425 | cpu: node_js_cpu(cpu).into(), 426 | } 427 | } 428 | } 429 | 430 | fn matches_os_or_cpu_vec(items: &[SmallStackString], target: &str) -> bool { 431 | if items.is_empty() { 432 | return true; 433 | } 434 | let mut had_negation = false; 435 | for item in items { 436 | if item.starts_with('!') { 437 | if &item[1..] == target { 438 | return false; 439 | } 440 | had_negation = true; 441 | } else if item == target { 442 | return true; 443 | } 444 | } 445 | had_negation 446 | } 447 | 448 | fn node_js_cpu(rust_arch: &str) -> &str { 449 | // possible values: https://nodejs.org/api/process.html#processarch 450 | // 'arm', 'arm64', 'ia32', 'mips','mipsel', 'ppc', 'ppc64', 's390', 's390x', and 'x64' 451 | match rust_arch { 452 | "x86_64" => "x64", 453 | "aarch64" => "arm64", 454 | value => value, 455 | } 456 | } 457 | 458 | fn node_js_os(rust_os: &str) -> &str { 459 | // possible values: https://nodejs.org/api/process.html#processplatform 460 | // 'aix', 'darwin', 'freebsd', 'linux', 'openbsd', 'sunos', and 'win32' 461 | match rust_os { 462 | "macos" => "darwin", 463 | "windows" => "win32", 464 | value => value, 465 | } 466 | } 467 | 468 | #[cfg(test)] 469 | mod test { 470 | use super::*; 471 | 472 | #[test] 473 | fn serialize_npm_package_id() { 474 | let id = NpmPackageId { 475 | nv: PackageNv::from_str("pkg-a@1.2.3").unwrap(), 476 | peer_dependencies: NpmPackageIdPeerDependencies::from([ 477 | NpmPackageId { 478 | nv: PackageNv::from_str("pkg-b@3.2.1").unwrap(), 479 | peer_dependencies: NpmPackageIdPeerDependencies::from([ 480 | NpmPackageId { 481 | nv: PackageNv::from_str("pkg-c@1.3.2").unwrap(), 482 | peer_dependencies: Default::default(), 483 | }, 484 | NpmPackageId { 485 | nv: PackageNv::from_str("pkg-d@2.3.4").unwrap(), 486 | peer_dependencies: Default::default(), 487 | }, 488 | ]), 489 | }, 490 | NpmPackageId { 491 | nv: PackageNv::from_str("pkg-e@2.3.1").unwrap(), 492 | peer_dependencies: NpmPackageIdPeerDependencies::from([ 493 | NpmPackageId { 494 | nv: PackageNv::from_str("pkg-f@2.3.1").unwrap(), 495 | peer_dependencies: Default::default(), 496 | }, 497 | ]), 498 | }, 499 | ]), 500 | }; 501 | 502 | // this shouldn't change because it's used in the lockfile 503 | let serialized = id.as_serialized(); 504 | assert_eq!( 505 | serialized, 506 | "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" 507 | ); 508 | assert_eq!(NpmPackageId::from_serialized(&serialized).unwrap(), id); 509 | } 510 | 511 | #[test] 512 | fn parse_npm_package_id() { 513 | #[track_caller] 514 | fn run_test(input: &str) { 515 | let id = NpmPackageId::from_serialized(input).unwrap(); 516 | assert_eq!(id.as_serialized(), input); 517 | } 518 | 519 | run_test("pkg-a@1.2.3"); 520 | run_test("pkg-a@1.2.3_pkg-b@3.2.1"); 521 | run_test( 522 | "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", 523 | ); 524 | 525 | #[track_caller] 526 | fn run_error_test(input: &str, message: &str) { 527 | let err = NpmPackageId::from_serialized(input).unwrap_err(); 528 | assert_eq!(format!("{:#}", err), message); 529 | } 530 | 531 | run_error_test( 532 | "asdf", 533 | "Invalid npm package id 'asdf'. Unexpected character. 534 | asdf 535 | ~", 536 | ); 537 | run_error_test( 538 | "asdf@test", 539 | "Invalid npm package id 'asdf@test'. Invalid npm version. Unexpected character. 540 | test 541 | ~", 542 | ); 543 | run_error_test( 544 | "pkg@1.2.3_asdf@test", 545 | "Invalid npm package id 'pkg@1.2.3_asdf@test'. Invalid npm version. Unexpected character. 546 | test 547 | ~", 548 | ); 549 | } 550 | 551 | #[test] 552 | fn test_matches_os_or_cpu_vec() { 553 | assert!(matches_os_or_cpu_vec(&[], "x64")); 554 | assert!(matches_os_or_cpu_vec(&["x64".into()], "x64")); 555 | assert!(!matches_os_or_cpu_vec(&["!x64".into()], "x64")); 556 | assert!(matches_os_or_cpu_vec(&["!arm64".into()], "x64")); 557 | assert!(matches_os_or_cpu_vec( 558 | &["!arm64".into(), "!x86".into()], 559 | "x64" 560 | )); 561 | assert!(!matches_os_or_cpu_vec( 562 | &["!arm64".into(), "!x86".into()], 563 | "x86" 564 | )); 565 | assert!(!matches_os_or_cpu_vec( 566 | &["!arm64".into(), "!x86".into(), "other".into()], 567 | "x86" 568 | )); 569 | 570 | // not explicitly excluded and there's an include, so it's considered a match 571 | assert!(matches_os_or_cpu_vec( 572 | &["!arm64".into(), "!x86".into(), "other".into()], 573 | "x64" 574 | )); 575 | } 576 | } 577 | -------------------------------------------------------------------------------- /src/resolution/common.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. MIT license. 2 | 3 | use std::collections::BTreeSet; 4 | use std::collections::HashMap; 5 | use std::sync::Arc; 6 | 7 | use deno_semver::StackString; 8 | use deno_semver::Version; 9 | use deno_semver::VersionReq; 10 | use deno_semver::WILDCARD_VERSION_REQ; 11 | use deno_semver::package::PackageName; 12 | use deno_semver::package::PackageNv; 13 | use thiserror::Error; 14 | 15 | use crate::registry::NpmPackageInfo; 16 | use crate::registry::NpmPackageVersionInfo; 17 | use crate::registry::NpmPackageVersionInfosIterator; 18 | 19 | /// Error that occurs when the version is not found in the package information. 20 | #[derive(Debug, Error, Clone, deno_error::JsError)] 21 | #[class(type)] 22 | #[error("Could not find version '{}' for npm package '{}'.", .0.version, .0.name)] 23 | pub struct NpmPackageVersionNotFound(pub PackageNv); 24 | 25 | #[derive(Debug, Error, Clone, deno_error::JsError)] 26 | pub enum NpmPackageVersionResolutionError { 27 | #[class(type)] 28 | #[error( 29 | "Could not find dist-tag '{dist_tag}' for npm package '{package_name}'." 30 | )] 31 | DistTagNotFound { 32 | dist_tag: String, 33 | package_name: StackString, 34 | }, 35 | #[class(type)] 36 | #[error( 37 | "Could not find version '{version}' referenced in dist-tag '{dist_tag}' for npm package '{package_name}'." 38 | )] 39 | DistTagVersionNotFound { 40 | package_name: StackString, 41 | dist_tag: String, 42 | version: String, 43 | }, 44 | #[class(type)] 45 | #[error( 46 | "Failed resolving tag '{package_name}@{dist_tag}' mapped to '{package_name}@{version}' because the package version was published at {publish_date}, but dependencies newer than {newest_dependency_date} are not allowed because it is newer than the specified minimum dependency date." 47 | )] 48 | DistTagVersionTooNew { 49 | package_name: StackString, 50 | dist_tag: String, 51 | version: String, 52 | publish_date: chrono::DateTime, 53 | newest_dependency_date: NewestDependencyDate, 54 | }, 55 | #[class(inherit)] 56 | #[error(transparent)] 57 | VersionNotFound(#[from] NpmPackageVersionNotFound), 58 | #[class(type)] 59 | #[error( 60 | "Could not find npm package '{}' matching '{}'.{}", package_name, version_req, newest_dependency_date.map(|v| format!("\n\nA newer matching version was found, but it was not used because it was newer than the specified minimum dependency date of {}.", v)).unwrap_or_else(String::new) 61 | )] 62 | VersionReqNotMatched { 63 | package_name: StackString, 64 | version_req: VersionReq, 65 | newest_dependency_date: Option, 66 | }, 67 | } 68 | 69 | #[derive(Debug, Default, Clone, Copy)] 70 | pub struct NewestDependencyDate(pub chrono::DateTime); 71 | 72 | impl std::fmt::Display for NewestDependencyDate { 73 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 74 | write!(f, "{}", self.0) 75 | } 76 | } 77 | 78 | impl NewestDependencyDate { 79 | pub fn matches(&self, date: chrono::DateTime) -> bool { 80 | date < self.0 81 | } 82 | } 83 | 84 | #[derive(Debug, Default, Clone)] 85 | pub struct NewestDependencyDateOptions { 86 | /// Prevents installing packages newer than the specified date. 87 | pub date: Option, 88 | pub exclude: BTreeSet, 89 | } 90 | 91 | impl NewestDependencyDateOptions { 92 | pub fn from_date(date: chrono::DateTime) -> Self { 93 | Self { 94 | date: Some(NewestDependencyDate(date)), 95 | exclude: Default::default(), 96 | } 97 | } 98 | 99 | pub fn get_for_package( 100 | &self, 101 | package_name: &PackageName, 102 | ) -> Option { 103 | let date = self.date?; 104 | if self.exclude.contains(package_name) { 105 | None 106 | } else { 107 | Some(date) 108 | } 109 | } 110 | } 111 | 112 | #[derive(Debug, Default, Clone)] 113 | pub struct NpmVersionResolver { 114 | /// Known good version requirement to use for the `@types/node` package 115 | /// when the version is unspecified or "latest". 116 | pub types_node_version_req: Option, 117 | /// Packages that are marked as "links" in the config file. 118 | pub link_packages: Arc>>, 119 | pub newest_dependency_date_options: NewestDependencyDateOptions, 120 | } 121 | 122 | impl NpmVersionResolver { 123 | pub fn get_for_package<'a>( 124 | &'a self, 125 | info: &'a NpmPackageInfo, 126 | ) -> NpmPackageVersionResolver<'a> { 127 | NpmPackageVersionResolver { 128 | info, 129 | newest_dependency_date: self 130 | .newest_dependency_date_options 131 | .get_for_package(&info.name), 132 | link_packages: self.link_packages.get(&info.name), 133 | types_node_version_req: self.types_node_version_req.as_ref(), 134 | } 135 | } 136 | } 137 | 138 | pub struct NpmPackageVersionResolver<'a> { 139 | info: &'a NpmPackageInfo, 140 | link_packages: Option<&'a Vec>, 141 | newest_dependency_date: Option, 142 | types_node_version_req: Option<&'a VersionReq>, 143 | } 144 | 145 | impl<'a> NpmPackageVersionResolver<'a> { 146 | pub fn info(&self) -> &'a NpmPackageInfo { 147 | self.info 148 | } 149 | 150 | /// Gets the version infos that match the link packages and newest dependency date. 151 | pub fn applicable_version_infos(&self) -> NpmPackageVersionInfosIterator<'a> { 152 | NpmPackageVersionInfosIterator::new( 153 | self.info, 154 | self.link_packages, 155 | self.newest_dependency_date, 156 | ) 157 | } 158 | 159 | pub fn version_req_satisfies_and_matches_newest_dependency_date( 160 | &self, 161 | version_req: &VersionReq, 162 | version: &Version, 163 | ) -> Result { 164 | Ok( 165 | self.version_req_satisfies(version_req, version)? 166 | && self.matches_newest_dependency_date(version), 167 | ) 168 | } 169 | 170 | pub fn version_req_satisfies( 171 | &self, 172 | version_req: &VersionReq, 173 | version: &Version, 174 | ) -> Result { 175 | match version_req.tag() { 176 | Some(tag) => { 177 | let version_info = self.tag_to_version_info(tag)?; 178 | Ok(version_info.version == *version) 179 | } 180 | None => { 181 | // For when someone just specifies @types/node, we want to pull in a 182 | // "known good" version of @types/node that works well with Deno and 183 | // not necessarily the latest version. For example, we might only be 184 | // compatible with Node vX, but then Node vY is published so we wouldn't 185 | // want to pull that in. 186 | // Note: If the user doesn't want this behavior, then they can specify an 187 | // explicit version. 188 | if self.info.name == "@types/node" 189 | && *version_req == *WILDCARD_VERSION_REQ 190 | && let Some(version_req) = self.types_node_version_req 191 | { 192 | return Ok(version_req.matches(version)); 193 | } 194 | 195 | Ok(version_req.matches(version)) 196 | } 197 | } 198 | } 199 | 200 | /// Gets if the provided version should be ignored or not 201 | /// based on the `newest_dependency_date`. 202 | pub fn matches_newest_dependency_date(&self, version: &Version) -> bool { 203 | match self.newest_dependency_date { 204 | Some(newest_dependency_date) => match self.info.time.get(version) { 205 | Some(date) => newest_dependency_date.matches(*date), 206 | None => true, 207 | }, 208 | None => true, 209 | } 210 | } 211 | 212 | pub fn resolve_best_package_version_info<'version>( 213 | &self, 214 | version_req: &VersionReq, 215 | existing_versions: impl Iterator, 216 | ) -> Result<&'a NpmPackageVersionInfo, NpmPackageVersionResolutionError> { 217 | // always attempt to resolve from the linked packages first 218 | if let Some(version_infos) = self.link_packages { 219 | let mut best_version: Option<&'a NpmPackageVersionInfo> = None; 220 | for version_info in version_infos { 221 | let version = &version_info.version; 222 | if self.version_req_satisfies(version_req, version)? { 223 | let is_greater = 224 | best_version.map(|c| *version > c.version).unwrap_or(true); 225 | if is_greater { 226 | best_version = Some(version_info); 227 | } 228 | } 229 | } 230 | if let Some(top_version) = best_version { 231 | return Ok(top_version); 232 | } 233 | } 234 | 235 | if let Some(version) = self 236 | .resolve_best_from_existing_versions(version_req, existing_versions)? 237 | { 238 | match self.info.versions.get(version) { 239 | Some(version_info) => Ok(version_info), 240 | None => Err(NpmPackageVersionResolutionError::VersionNotFound( 241 | NpmPackageVersionNotFound(PackageNv { 242 | name: self.info.name.clone(), 243 | version: version.clone(), 244 | }), 245 | )), 246 | } 247 | } else { 248 | // get the information 249 | self.get_resolved_package_version_and_info(version_req) 250 | } 251 | } 252 | 253 | fn get_resolved_package_version_and_info( 254 | &self, 255 | version_req: &VersionReq, 256 | ) -> Result<&'a NpmPackageVersionInfo, NpmPackageVersionResolutionError> { 257 | let mut found_matching_version = false; 258 | if let Some(tag) = version_req.tag() { 259 | self.tag_to_version_info(tag) 260 | // When the version is *, if there is a latest tag, use it directly. 261 | // No need to care about @types/node here, because it'll be handled specially below. 262 | } else if self.info.dist_tags.contains_key("latest") 263 | && self.info.name != "@types/node" 264 | // When the latest tag satisfies the version requirement, use it directly. 265 | // https://github.com/npm/npm-pick-manifest/blob/67508da8e21f7317e3159765006da0d6a0a61f84/lib/index.js#L125 266 | && self.info 267 | .dist_tags 268 | .get("latest") 269 | .filter(|version| self.matches_newest_dependency_date(version)) 270 | .map(|version| { 271 | *version_req == *WILDCARD_VERSION_REQ || 272 | self.version_req_satisfies(version_req, version).ok().unwrap_or(false) 273 | }) 274 | .unwrap_or(false) 275 | { 276 | self.tag_to_version_info("latest") 277 | } else { 278 | let mut maybe_best_version: Option<&'a NpmPackageVersionInfo> = None; 279 | for version_info in self.info.versions.values() { 280 | let version = &version_info.version; 281 | if self.version_req_satisfies(version_req, version)? { 282 | found_matching_version = true; 283 | if self.matches_newest_dependency_date(version) { 284 | let is_best_version = maybe_best_version 285 | .as_ref() 286 | .map(|best_version| best_version.version.cmp(version).is_lt()) 287 | .unwrap_or(true); 288 | if is_best_version { 289 | maybe_best_version = Some(version_info); 290 | } 291 | } 292 | } 293 | } 294 | 295 | match maybe_best_version { 296 | Some(v) => Ok(v), 297 | // Although it seems like we could make this smart by fetching the latest 298 | // information for this package here, we really need a full restart. There 299 | // could be very interesting bugs that occur if this package's version was 300 | // resolved by something previous using the old information, then now being 301 | // smart here causes a new fetch of the package information, meaning this 302 | // time the previous resolution of this package's version resolved to an older 303 | // version, but next time to a different version because it has new information. 304 | None => Err(NpmPackageVersionResolutionError::VersionReqNotMatched { 305 | package_name: self.info.name.clone(), 306 | version_req: version_req.clone(), 307 | newest_dependency_date: found_matching_version 308 | .then_some(self.newest_dependency_date) 309 | .flatten(), 310 | }), 311 | } 312 | } 313 | } 314 | 315 | fn resolve_best_from_existing_versions<'b>( 316 | &self, 317 | version_req: &VersionReq, 318 | existing_versions: impl Iterator, 319 | ) -> Result, NpmPackageVersionResolutionError> { 320 | let mut maybe_best_version: Option<&Version> = None; 321 | for version in existing_versions { 322 | if self.version_req_satisfies(version_req, version)? { 323 | let is_best_version = maybe_best_version 324 | .as_ref() 325 | .map(|best_version| (*best_version).cmp(version).is_lt()) 326 | .unwrap_or(true); 327 | if is_best_version { 328 | maybe_best_version = Some(version); 329 | } 330 | } 331 | } 332 | Ok(maybe_best_version) 333 | } 334 | 335 | fn tag_to_version_info( 336 | &self, 337 | tag: &str, 338 | ) -> Result<&'a NpmPackageVersionInfo, NpmPackageVersionResolutionError> { 339 | if let Some(version) = self.info.dist_tags.get(tag) { 340 | match self.info.versions.get(version) { 341 | Some(version_info) => { 342 | if self.matches_newest_dependency_date(version) { 343 | Ok(version_info) 344 | } else { 345 | Err(NpmPackageVersionResolutionError::DistTagVersionTooNew { 346 | package_name: self.info.name.clone(), 347 | dist_tag: tag.to_string(), 348 | version: version.to_string(), 349 | newest_dependency_date: self.newest_dependency_date.unwrap(), 350 | publish_date: *self.info.time.get(version).unwrap(), 351 | }) 352 | } 353 | } 354 | None => Err(NpmPackageVersionResolutionError::DistTagVersionNotFound { 355 | package_name: self.info.name.clone(), 356 | dist_tag: tag.to_string(), 357 | version: version.to_string(), 358 | }), 359 | } 360 | } else { 361 | Err(NpmPackageVersionResolutionError::DistTagNotFound { 362 | package_name: self.info.name.clone(), 363 | dist_tag: tag.to_string(), 364 | }) 365 | } 366 | } 367 | } 368 | 369 | #[cfg(test)] 370 | mod test { 371 | use std::collections::HashMap; 372 | 373 | use deno_semver::package::PackageReq; 374 | 375 | use super::*; 376 | 377 | #[test] 378 | fn test_get_resolved_package_version_and_info() { 379 | // dist tag where version doesn't exist 380 | let package_req = PackageReq::from_str("test@latest").unwrap(); 381 | let package_info = NpmPackageInfo { 382 | name: "test".into(), 383 | versions: HashMap::new(), 384 | dist_tags: HashMap::from([( 385 | "latest".into(), 386 | Version::parse_from_npm("1.0.0-alpha").unwrap(), 387 | )]), 388 | time: Default::default(), 389 | }; 390 | let resolver = NpmVersionResolver { 391 | types_node_version_req: None, 392 | link_packages: Default::default(), 393 | newest_dependency_date_options: Default::default(), 394 | }; 395 | let package_version_resolver = resolver.get_for_package(&package_info); 396 | let result = package_version_resolver 397 | .get_resolved_package_version_and_info(&package_req.version_req); 398 | assert_eq!( 399 | result.err().unwrap().to_string(), 400 | "Could not find version '1.0.0-alpha' referenced in dist-tag 'latest' for npm package 'test'." 401 | ); 402 | 403 | // dist tag where version is a pre-release 404 | let package_req = PackageReq::from_str("test@latest").unwrap(); 405 | let package_info = NpmPackageInfo { 406 | name: "test".into(), 407 | versions: HashMap::from([ 408 | ( 409 | Version::parse_from_npm("0.1.0").unwrap(), 410 | NpmPackageVersionInfo::default(), 411 | ), 412 | ( 413 | Version::parse_from_npm("1.0.0-alpha").unwrap(), 414 | NpmPackageVersionInfo { 415 | version: Version::parse_from_npm("1.0.0-alpha").unwrap(), 416 | ..Default::default() 417 | }, 418 | ), 419 | ]), 420 | dist_tags: HashMap::from([( 421 | "latest".into(), 422 | Version::parse_from_npm("1.0.0-alpha").unwrap(), 423 | )]), 424 | time: Default::default(), 425 | }; 426 | let version_resolver = resolver.get_for_package(&package_info); 427 | let result = version_resolver 428 | .get_resolved_package_version_and_info(&package_req.version_req); 429 | assert_eq!(result.unwrap().version.to_string(), "1.0.0-alpha"); 430 | } 431 | 432 | #[test] 433 | fn test_types_node_version() { 434 | // this will use the 1.0.0 version because that's what was specified 435 | // for the "types_node_version_req" even though the latest is 1.1.0 436 | let package_req = PackageReq::from_str("@types/node").unwrap(); 437 | let package_info = NpmPackageInfo { 438 | name: "@types/node".into(), 439 | versions: HashMap::from([ 440 | ( 441 | Version::parse_from_npm("1.0.0").unwrap(), 442 | NpmPackageVersionInfo { 443 | version: Version::parse_from_npm("1.0.0").unwrap(), 444 | ..Default::default() 445 | }, 446 | ), 447 | ( 448 | Version::parse_from_npm("1.1.0").unwrap(), 449 | NpmPackageVersionInfo { 450 | version: Version::parse_from_npm("1.1.0").unwrap(), 451 | ..Default::default() 452 | }, 453 | ), 454 | ]), 455 | dist_tags: HashMap::from([( 456 | "latest".into(), 457 | Version::parse_from_npm("1.1.0").unwrap(), 458 | )]), 459 | time: Default::default(), 460 | }; 461 | let resolver = NpmVersionResolver { 462 | types_node_version_req: Some( 463 | VersionReq::parse_from_npm("1.0.0").unwrap(), 464 | ), 465 | link_packages: Default::default(), 466 | newest_dependency_date_options: Default::default(), 467 | }; 468 | let version_resolver = resolver.get_for_package(&package_info); 469 | let result = version_resolver 470 | .get_resolved_package_version_and_info(&package_req.version_req); 471 | assert_eq!(result.unwrap().version.to_string(), "1.0.0"); 472 | } 473 | 474 | #[test] 475 | fn test_wildcard_version_req() { 476 | let package_req = PackageReq::from_str("some-pkg").unwrap(); 477 | let package_info = NpmPackageInfo { 478 | name: "some-pkg".into(), 479 | versions: HashMap::from([ 480 | ( 481 | Version::parse_from_npm("1.0.0-rc.1").unwrap(), 482 | NpmPackageVersionInfo { 483 | version: Version::parse_from_npm("1.0.0-rc.1").unwrap(), 484 | ..Default::default() 485 | }, 486 | ), 487 | ( 488 | Version::parse_from_npm("2.0.0").unwrap(), 489 | NpmPackageVersionInfo { 490 | version: Version::parse_from_npm("2.0.0").unwrap(), 491 | ..Default::default() 492 | }, 493 | ), 494 | ]), 495 | dist_tags: HashMap::from([( 496 | "latest".into(), 497 | Version::parse_from_npm("1.0.0-rc.1").unwrap(), 498 | )]), 499 | time: Default::default(), 500 | }; 501 | let resolver = NpmVersionResolver { 502 | types_node_version_req: None, 503 | link_packages: Default::default(), 504 | newest_dependency_date_options: Default::default(), 505 | }; 506 | let version_resolver = resolver.get_for_package(&package_info); 507 | let result = version_resolver 508 | .get_resolved_package_version_and_info(&package_req.version_req); 509 | assert_eq!(result.unwrap().version.to_string(), "1.0.0-rc.1"); 510 | } 511 | 512 | #[test] 513 | fn test_latest_tag_version_req() { 514 | let package_info = NpmPackageInfo { 515 | name: "some-pkg".into(), 516 | versions: HashMap::from([ 517 | ( 518 | Version::parse_from_npm("0.1.0-alpha.1").unwrap(), 519 | NpmPackageVersionInfo { 520 | version: Version::parse_from_npm("0.1.0-alpha.1").unwrap(), 521 | ..Default::default() 522 | }, 523 | ), 524 | ( 525 | Version::parse_from_npm("0.1.0-alpha.2").unwrap(), 526 | NpmPackageVersionInfo { 527 | version: Version::parse_from_npm("0.1.0-alpha.2").unwrap(), 528 | ..Default::default() 529 | }, 530 | ), 531 | ( 532 | Version::parse_from_npm("0.1.0-beta.1").unwrap(), 533 | NpmPackageVersionInfo { 534 | version: Version::parse_from_npm("0.1.0-beta.1").unwrap(), 535 | ..Default::default() 536 | }, 537 | ), 538 | ( 539 | Version::parse_from_npm("0.1.0-beta.2").unwrap(), 540 | NpmPackageVersionInfo { 541 | version: Version::parse_from_npm("0.1.0-beta.2").unwrap(), 542 | ..Default::default() 543 | }, 544 | ), 545 | ]), 546 | dist_tags: HashMap::from([ 547 | ( 548 | "latest".into(), 549 | Version::parse_from_npm("0.1.0-alpha.2").unwrap(), 550 | ), 551 | ( 552 | "dev".into(), 553 | Version::parse_from_npm("0.1.0-beta.2").unwrap(), 554 | ), 555 | ]), 556 | time: Default::default(), 557 | }; 558 | let resolver = NpmVersionResolver { 559 | types_node_version_req: None, 560 | link_packages: Default::default(), 561 | newest_dependency_date_options: Default::default(), 562 | }; 563 | 564 | // check for when matches dist tag 565 | let package_req = PackageReq::from_str("some-pkg@^0.1.0-alpha.2").unwrap(); 566 | let version_resolver = resolver.get_for_package(&package_info); 567 | let result = version_resolver 568 | .get_resolved_package_version_and_info(&package_req.version_req); 569 | assert_eq!( 570 | result.unwrap().version.to_string(), 571 | "0.1.0-alpha.2" // not "0.1.0-beta.2" 572 | ); 573 | 574 | // check for when not matches dist tag 575 | let package_req = PackageReq::from_str("some-pkg@^0.1.0-beta.2").unwrap(); 576 | let result = version_resolver 577 | .get_resolved_package_version_and_info(&package_req.version_req); 578 | assert_eq!(result.unwrap().version.to_string(), "0.1.0-beta.2"); 579 | } 580 | } 581 | -------------------------------------------------------------------------------- /src/npm_rc/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. MIT license. 2 | 3 | use monch::*; 4 | use std::borrow::Cow; 5 | use std::collections::HashMap; 6 | use std::sync::Arc; 7 | use url::Url; 8 | 9 | use self::ini::Key; 10 | use self::ini::KeyValueOrSection; 11 | use self::ini::Value; 12 | 13 | mod ini; 14 | 15 | #[derive(Debug, thiserror::Error)] 16 | pub enum ResolveError { 17 | #[error("failed parsing npm registry url for scope '{scope}'")] 18 | UrlScope { 19 | scope: String, 20 | #[source] 21 | source: url::ParseError, 22 | }, 23 | #[error("failed resolving .npmrc config for scope '{0}'")] 24 | NpmrcScope(String), 25 | #[error("failed parsing npm registry url")] 26 | Url(#[source] url::ParseError), 27 | } 28 | 29 | pub type NpmRcParseError = monch::ParseErrorFailureError; 30 | 31 | #[derive(Debug, Default, Clone, PartialEq, Eq)] 32 | pub struct RegistryConfig { 33 | pub auth: Option, 34 | pub auth_token: Option, 35 | pub username: Option, 36 | pub password: Option, 37 | pub email: Option, 38 | pub certfile: Option, 39 | pub keyfile: Option, 40 | } 41 | 42 | #[derive(Debug, Default, Clone, PartialEq, Eq)] 43 | pub struct NpmRc { 44 | pub registry: Option, 45 | pub scope_registries: HashMap, 46 | pub registry_configs: HashMap>, 47 | } 48 | 49 | impl NpmRc { 50 | pub fn parse( 51 | input: &str, 52 | get_env_var: &impl Fn(&str) -> Option, 53 | ) -> Result { 54 | let kv_or_sections = ini::parse_ini(input)?; 55 | let mut registry = None; 56 | let mut scope_registries: HashMap = HashMap::new(); 57 | let mut registry_configs: HashMap = HashMap::new(); 58 | 59 | for kv_or_section in kv_or_sections { 60 | match kv_or_section { 61 | KeyValueOrSection::KeyValue(kv) => { 62 | if let Key::Plain(key) = &kv.key { 63 | if let Some((left, right)) = key.rsplit_once(':') { 64 | if let Some(scope) = left.strip_prefix('@') { 65 | if right == "registry" 66 | && let Value::String(text) = &kv.value 67 | { 68 | let value = expand_vars(text, get_env_var); 69 | scope_registries.insert(scope.to_string(), value); 70 | } 71 | } else if let Some(host_and_path) = left.strip_prefix("//") 72 | && let Value::String(text) = &kv.value 73 | { 74 | let value = expand_vars(text, get_env_var); 75 | let config = registry_configs 76 | .entry(host_and_path.to_string()) 77 | .or_default(); 78 | match right { 79 | "_auth" => { 80 | config.auth = Some(value); 81 | } 82 | "_authToken" => { 83 | config.auth_token = Some(value); 84 | } 85 | "username" => { 86 | config.username = Some(value); 87 | } 88 | "_password" => { 89 | config.password = Some(value); 90 | } 91 | "email" => { 92 | config.email = Some(value); 93 | } 94 | "certfile" => { 95 | config.certfile = Some(value); 96 | } 97 | "keyfile" => { 98 | config.keyfile = Some(value); 99 | } 100 | _ => {} 101 | } 102 | } 103 | } else if key == "registry" 104 | && let Value::String(text) = &kv.value 105 | { 106 | let value = expand_vars(text, get_env_var); 107 | registry = Some(value); 108 | } 109 | } 110 | } 111 | KeyValueOrSection::Section(_) => { 112 | // ignore 113 | } 114 | } 115 | } 116 | 117 | Ok(NpmRc { 118 | registry, 119 | scope_registries, 120 | registry_configs: registry_configs 121 | .into_iter() 122 | .map(|(k, v)| (k, Arc::new(v))) 123 | .collect(), 124 | }) 125 | } 126 | 127 | pub fn as_resolved( 128 | &self, 129 | env_registry_url: &Url, 130 | ) -> Result { 131 | let mut scopes = HashMap::with_capacity(self.scope_registries.len()); 132 | for scope in self.scope_registries.keys() { 133 | let (url, config) = match self.registry_url_and_config_for_maybe_scope( 134 | Some(scope.as_str()), 135 | env_registry_url.as_str(), 136 | ) { 137 | Some((url, config)) => ( 138 | Url::parse(&url).map_err(|e| ResolveError::UrlScope { 139 | scope: scope.clone(), 140 | source: e, 141 | })?, 142 | config.clone(), 143 | ), 144 | None => { 145 | return Err(ResolveError::NpmrcScope(scope.clone())); 146 | } 147 | }; 148 | scopes.insert( 149 | scope.clone(), 150 | RegistryConfigWithUrl { 151 | registry_url: url, 152 | config, 153 | }, 154 | ); 155 | } 156 | let (default_url, default_config) = match self 157 | .registry_url_and_config_for_maybe_scope(None, env_registry_url.as_str()) 158 | { 159 | Some((default_url, default_config)) => ( 160 | Url::parse(&default_url).map_err(ResolveError::Url)?, 161 | default_config, 162 | ), 163 | None => ( 164 | env_registry_url.clone(), 165 | Arc::new(RegistryConfig::default()), 166 | ), 167 | }; 168 | Ok(ResolvedNpmRc { 169 | default_config: RegistryConfigWithUrl { 170 | registry_url: default_url, 171 | config: default_config, 172 | }, 173 | scopes, 174 | registry_configs: self.registry_configs.clone(), 175 | }) 176 | } 177 | 178 | fn registry_url_and_config_for_maybe_scope( 179 | &self, 180 | maybe_scope_name: Option<&str>, 181 | env_registry_url: &str, 182 | ) -> Option<(String, Arc)> { 183 | let registry_url = maybe_scope_name 184 | .and_then(|scope| self.scope_registries.get(scope).map(|s| s.as_str())) 185 | .or(self.registry.as_deref()) 186 | .unwrap_or(env_registry_url); 187 | 188 | let original_registry_url = if registry_url.ends_with('/') { 189 | Cow::Borrowed(registry_url) 190 | } else { 191 | Cow::Owned(format!("{}/", registry_url)) 192 | }; 193 | // https://example.com/ -> example.com/ 194 | let registry_url = original_registry_url 195 | .split_once("//") 196 | .map(|(_, right)| right)?; 197 | let mut url: &str = registry_url; 198 | 199 | loop { 200 | if let Some(config) = self.registry_configs.get(url) { 201 | return Some((original_registry_url.into_owned(), config.clone())); 202 | } 203 | let Some(next_slash_index) = url[..url.len() - 1].rfind('/') else { 204 | if original_registry_url == env_registry_url { 205 | return None; 206 | } 207 | return Some(( 208 | original_registry_url.into_owned(), 209 | Arc::new(RegistryConfig::default()), 210 | )); 211 | }; 212 | url = &url[..next_slash_index + 1]; 213 | } 214 | } 215 | } 216 | 217 | fn get_scope_name(package_name: &str) -> Option<&str> { 218 | let no_at_pkg_name = package_name.strip_prefix('@')?; 219 | no_at_pkg_name.split_once('/').map(|(scope, _)| scope) 220 | } 221 | 222 | #[derive(Debug, Clone, PartialEq, Eq)] 223 | pub struct RegistryConfigWithUrl { 224 | pub registry_url: Url, 225 | pub config: Arc, 226 | } 227 | 228 | #[derive(Debug, Clone, PartialEq, Eq)] 229 | pub struct ResolvedNpmRc { 230 | pub default_config: RegistryConfigWithUrl, 231 | pub scopes: HashMap, 232 | pub registry_configs: HashMap>, 233 | } 234 | 235 | impl ResolvedNpmRc { 236 | pub fn get_registry_url(&self, package_name: &str) -> &Url { 237 | let Some(scope_name) = get_scope_name(package_name) else { 238 | return &self.default_config.registry_url; 239 | }; 240 | 241 | match self.scopes.get(scope_name) { 242 | Some(registry_config) => ®istry_config.registry_url, 243 | None => &self.default_config.registry_url, 244 | } 245 | } 246 | 247 | pub fn get_registry_config( 248 | &self, 249 | package_name: &str, 250 | ) -> &Arc { 251 | let Some(scope_name) = get_scope_name(package_name) else { 252 | return &self.default_config.config; 253 | }; 254 | 255 | match self.scopes.get(scope_name) { 256 | Some(registry_config) => ®istry_config.config, 257 | None => &self.default_config.config, 258 | } 259 | } 260 | 261 | pub fn get_all_known_registries_urls(&self) -> Vec { 262 | let mut urls = Vec::with_capacity(1 + self.scopes.len()); 263 | 264 | urls.push(self.default_config.registry_url.clone()); 265 | for scope_config in self.scopes.values() { 266 | urls.push(scope_config.registry_url.clone()); 267 | } 268 | urls 269 | } 270 | 271 | pub fn tarball_config( 272 | &self, 273 | tarball_url: &Url, 274 | ) -> Option<&Arc> { 275 | // https://example.com/chalk.tgz -> example.com/.tgz 276 | let registry_url = tarball_url 277 | .as_str() 278 | .split_once("//") 279 | .map(|(_, right)| right)?; 280 | let mut best_match: Option<(&str, &Arc)> = None; 281 | for (config_url, config) in &self.registry_configs { 282 | if registry_url.starts_with(config_url) 283 | && (best_match.is_none() 284 | || matches!(best_match, Some((current_config_url, _)) if config_url.len() > current_config_url.len())) 285 | { 286 | best_match = Some((config_url, config)); 287 | } 288 | } 289 | best_match.map(|(_, config)| config) 290 | } 291 | } 292 | 293 | fn expand_vars( 294 | input: &str, 295 | get_env_var: &impl Fn(&str) -> Option, 296 | ) -> String { 297 | fn escaped_char(input: &str) -> ParseResult<'_, char> { 298 | preceded(ch('\\'), next_char)(input) 299 | } 300 | 301 | fn env_var(input: &str) -> ParseResult<'_, &str> { 302 | let (input, _) = tag("${")(input)?; 303 | let (input, var_name) = take_while(|c| c != '}')(input)?; 304 | if var_name.chars().any(|c| matches!(c, '$' | '{' | '\\')) { 305 | return ParseError::backtrace(); 306 | } 307 | let (input, _) = ch('}')(input)?; 308 | Ok((input, var_name)) 309 | } 310 | 311 | let (input, results) = many0(or3( 312 | map(escaped_char, |c| c.to_string()), 313 | map(env_var, |var_name| { 314 | if let Some(var_value) = get_env_var(var_name) { 315 | var_value 316 | } else { 317 | format!("${{{}}}", var_name) 318 | } 319 | }), 320 | map(next_char, |c| c.to_string()), 321 | ))(input) 322 | .unwrap(); 323 | assert!(input.is_empty()); 324 | results.join("") 325 | } 326 | 327 | #[cfg(test)] 328 | mod test { 329 | use super::*; 330 | 331 | use pretty_assertions::assert_eq; 332 | 333 | #[test] 334 | fn test_parse_basic() { 335 | // https://docs.npmjs.com/cli/v10/configuring-npm/npmrc#auth-related-configuration 336 | let npm_rc = NpmRc::parse( 337 | r#" 338 | @myorg:registry=https://example.com/myorg 339 | @another:registry=https://example.com/another 340 | @example:registry=https://example.com/example 341 | @yet_another:registry=https://yet.another.com/ 342 | //registry.npmjs.org/:_authToken=MYTOKEN 343 | ; would apply to both @myorg and @another 344 | //example.com/:_authToken=MYTOKEN0 345 | //example.com/:_auth=AUTH 346 | //example.com/:username=USERNAME 347 | //example.com/:_password=PASSWORD 348 | //example.com/:email=EMAIL 349 | //example.com/:certfile=CERTFILE 350 | //example.com/:keyfile=KEYFILE 351 | ; would apply only to @myorg 352 | //example.com/myorg/:_authToken=MYTOKEN1 353 | ; would apply only to @another 354 | //example.com/another/:_authToken=MYTOKEN2 355 | ; this should not apply to `@yet_another`, because the URL contains the name of the scope 356 | ; and not the URL of the registry root specified above 357 | //yet.another.com/yet_another/:_authToken=MYTOKEN3 358 | registry=https://registry.npmjs.org/ 359 | "#, 360 | &|_| None, 361 | ) 362 | .unwrap(); 363 | assert_eq!( 364 | npm_rc, 365 | NpmRc { 366 | registry: Some("https://registry.npmjs.org/".to_string()), 367 | scope_registries: HashMap::from([ 368 | ("myorg".to_string(), "https://example.com/myorg".to_string()), 369 | ( 370 | "another".to_string(), 371 | "https://example.com/another".to_string() 372 | ), 373 | ( 374 | "example".to_string(), 375 | "https://example.com/example".to_string() 376 | ), 377 | ( 378 | "yet_another".to_string(), 379 | "https://yet.another.com/".to_string() 380 | ), 381 | ]), 382 | registry_configs: HashMap::from([ 383 | ( 384 | "example.com/".to_string(), 385 | Arc::new(RegistryConfig { 386 | auth: Some("AUTH".to_string()), 387 | auth_token: Some("MYTOKEN0".to_string()), 388 | username: Some("USERNAME".to_string()), 389 | password: Some("PASSWORD".to_string()), 390 | email: Some("EMAIL".to_string()), 391 | certfile: Some("CERTFILE".to_string()), 392 | keyfile: Some("KEYFILE".to_string()), 393 | }) 394 | ), 395 | ( 396 | "example.com/another/".to_string(), 397 | Arc::new(RegistryConfig { 398 | auth_token: Some("MYTOKEN2".to_string()), 399 | ..Default::default() 400 | }) 401 | ), 402 | ( 403 | "example.com/myorg/".to_string(), 404 | Arc::new(RegistryConfig { 405 | auth_token: Some("MYTOKEN1".to_string()), 406 | ..Default::default() 407 | }) 408 | ), 409 | ( 410 | "yet.another.com/yet_another/".to_string(), 411 | Arc::new(RegistryConfig { 412 | auth_token: Some("MYTOKEN3".to_string()), 413 | ..Default::default() 414 | }) 415 | ), 416 | ( 417 | "registry.npmjs.org/".to_string(), 418 | Arc::new(RegistryConfig { 419 | auth_token: Some("MYTOKEN".to_string()), 420 | ..Default::default() 421 | }) 422 | ), 423 | ]) 424 | } 425 | ); 426 | 427 | let resolved_npm_rc = npm_rc 428 | .as_resolved(&Url::parse("https://deno.land/npm/").unwrap()) 429 | .unwrap(); 430 | assert_eq!( 431 | resolved_npm_rc, 432 | ResolvedNpmRc { 433 | default_config: RegistryConfigWithUrl { 434 | registry_url: Url::parse("https://registry.npmjs.org/").unwrap(), 435 | config: Arc::new(RegistryConfig { 436 | auth_token: Some("MYTOKEN".to_string()), 437 | ..Default::default() 438 | }), 439 | }, 440 | scopes: HashMap::from([ 441 | ( 442 | "myorg".to_string(), 443 | RegistryConfigWithUrl { 444 | registry_url: Url::parse("https://example.com/myorg/").unwrap(), 445 | config: Arc::new(RegistryConfig { 446 | auth_token: Some("MYTOKEN1".to_string()), 447 | ..Default::default() 448 | }) 449 | } 450 | ), 451 | ( 452 | "another".to_string(), 453 | RegistryConfigWithUrl { 454 | registry_url: Url::parse("https://example.com/another/").unwrap(), 455 | config: Arc::new(RegistryConfig { 456 | auth_token: Some("MYTOKEN2".to_string()), 457 | ..Default::default() 458 | }) 459 | } 460 | ), 461 | ( 462 | "example".to_string(), 463 | RegistryConfigWithUrl { 464 | registry_url: Url::parse("https://example.com/example/").unwrap(), 465 | config: Arc::new(RegistryConfig { 466 | auth: Some("AUTH".to_string()), 467 | auth_token: Some("MYTOKEN0".to_string()), 468 | username: Some("USERNAME".to_string()), 469 | password: Some("PASSWORD".to_string()), 470 | email: Some("EMAIL".to_string()), 471 | certfile: Some("CERTFILE".to_string()), 472 | keyfile: Some("KEYFILE".to_string()), 473 | }) 474 | } 475 | ), 476 | ( 477 | "yet_another".to_string(), 478 | RegistryConfigWithUrl { 479 | registry_url: Url::parse("https://yet.another.com/").unwrap(), 480 | config: Default::default() 481 | } 482 | ), 483 | ]), 484 | registry_configs: npm_rc.registry_configs.clone(), 485 | } 486 | ); 487 | 488 | // no matching scoped package 489 | { 490 | let registry_url = resolved_npm_rc.get_registry_url("test"); 491 | let config = resolved_npm_rc.get_registry_config("test"); 492 | assert_eq!(registry_url.as_str(), "https://registry.npmjs.org/"); 493 | assert_eq!(config.auth_token, Some("MYTOKEN".to_string())); 494 | } 495 | // matching scoped package 496 | { 497 | let registry_url = resolved_npm_rc.get_registry_url("@example/pkg"); 498 | let config = resolved_npm_rc.get_registry_config("@example/pkg"); 499 | assert_eq!(registry_url.as_str(), "https://example.com/example/"); 500 | assert_eq!(config.auth_token, Some("MYTOKEN0".to_string())); 501 | } 502 | // matching scoped package with specific token 503 | { 504 | let registry_url = resolved_npm_rc.get_registry_url("@myorg/pkg"); 505 | let config = resolved_npm_rc.get_registry_config("@myorg/pkg"); 506 | assert_eq!(registry_url.as_str(), "https://example.com/myorg/"); 507 | assert_eq!(config.auth_token, Some("MYTOKEN1".to_string())); 508 | } 509 | // This should not return the token - the configuration is borked for `@yet_another` scope - 510 | // it defines the registry url as root + scope_name and instead it should be matching the 511 | // registry root. 512 | { 513 | let registry_url = resolved_npm_rc.get_registry_url("@yet_another/pkg"); 514 | let config = resolved_npm_rc.get_registry_config("@yet_another/pkg"); 515 | assert_eq!(registry_url.as_str(), "https://yet.another.com/"); 516 | assert_eq!(config.auth_token, None); 517 | } 518 | 519 | assert_eq!( 520 | resolved_npm_rc.get_registry_url("@deno/test").as_str(), 521 | "https://registry.npmjs.org/" 522 | ); 523 | assert_eq!( 524 | resolved_npm_rc 525 | .get_registry_config("@deno/test") 526 | .auth_token 527 | .as_ref() 528 | .unwrap(), 529 | "MYTOKEN" 530 | ); 531 | 532 | assert_eq!( 533 | resolved_npm_rc.get_registry_url("@myorg/test").as_str(), 534 | "https://example.com/myorg/" 535 | ); 536 | assert_eq!( 537 | resolved_npm_rc 538 | .get_registry_config("@myorg/test") 539 | .auth_token 540 | .as_ref() 541 | .unwrap(), 542 | "MYTOKEN1" 543 | ); 544 | 545 | assert_eq!( 546 | resolved_npm_rc.get_registry_url("@another/test").as_str(), 547 | "https://example.com/another/" 548 | ); 549 | assert_eq!( 550 | resolved_npm_rc 551 | .get_registry_config("@another/test") 552 | .auth_token 553 | .as_ref() 554 | .unwrap(), 555 | "MYTOKEN2" 556 | ); 557 | 558 | assert_eq!( 559 | resolved_npm_rc.get_registry_url("@example/test").as_str(), 560 | "https://example.com/example/" 561 | ); 562 | let config = resolved_npm_rc.get_registry_config("@example/test"); 563 | assert_eq!(config.auth.as_ref().unwrap(), "AUTH"); 564 | assert_eq!(config.auth_token.as_ref().unwrap(), "MYTOKEN0"); 565 | assert_eq!(config.username.as_ref().unwrap(), "USERNAME"); 566 | assert_eq!(config.password.as_ref().unwrap(), "PASSWORD"); 567 | assert_eq!(config.email.as_ref().unwrap(), "EMAIL"); 568 | assert_eq!(config.certfile.as_ref().unwrap(), "CERTFILE"); 569 | assert_eq!(config.keyfile.as_ref().unwrap(), "KEYFILE"); 570 | 571 | // tarball uri 572 | { 573 | assert_eq!( 574 | resolved_npm_rc 575 | .tarball_config( 576 | &Url::parse("https://example.com/example/chalk.tgz").unwrap(), 577 | ) 578 | .unwrap() 579 | .auth_token 580 | .as_ref() 581 | .unwrap(), 582 | "MYTOKEN0" 583 | ); 584 | assert_eq!( 585 | resolved_npm_rc 586 | .tarball_config( 587 | &Url::parse("https://example.com/myorg/chalk.tgz").unwrap(), 588 | ) 589 | .unwrap() 590 | .auth_token 591 | .as_ref() 592 | .unwrap(), 593 | "MYTOKEN1" 594 | ); 595 | assert_eq!( 596 | resolved_npm_rc 597 | .tarball_config( 598 | &Url::parse("https://example.com/another/chalk.tgz").unwrap(), 599 | ) 600 | .unwrap() 601 | .auth_token 602 | .as_ref() 603 | .unwrap(), 604 | "MYTOKEN2" 605 | ); 606 | assert_eq!( 607 | resolved_npm_rc.tarball_config( 608 | &Url::parse("https://yet.another.com/example/chalk.tgz").unwrap(), 609 | ), 610 | None, 611 | ); 612 | assert_eq!( 613 | resolved_npm_rc 614 | .tarball_config( 615 | &Url::parse( 616 | "https://yet.another.com/yet_another/example/chalk.tgz" 617 | ) 618 | .unwrap(), 619 | ) 620 | .unwrap() 621 | .auth_token 622 | .as_ref() 623 | .unwrap(), 624 | "MYTOKEN3" 625 | ); 626 | } 627 | } 628 | 629 | #[test] 630 | fn test_parse_env_vars() { 631 | let npm_rc = NpmRc::parse( 632 | r#" 633 | @myorg:registry=${VAR_FOUND} 634 | @another:registry=${VAR_NOT_FOUND} 635 | @a:registry=\${VAR_FOUND} 636 | //registry.npmjs.org/:_authToken=${VAR_FOUND} 637 | registry=${VAR_FOUND} 638 | "#, 639 | &|var_name| match var_name { 640 | "VAR_FOUND" => Some("SOME_VALUE".to_string()), 641 | _ => None, 642 | }, 643 | ) 644 | .unwrap(); 645 | assert_eq!( 646 | npm_rc, 647 | NpmRc { 648 | registry: Some("SOME_VALUE".to_string()), 649 | scope_registries: HashMap::from([ 650 | ("a".to_string(), "${VAR_FOUND}".to_string()), 651 | ("myorg".to_string(), "SOME_VALUE".to_string()), 652 | ("another".to_string(), "${VAR_NOT_FOUND}".to_string()), 653 | ]), 654 | registry_configs: HashMap::from([( 655 | "registry.npmjs.org/".to_string(), 656 | Arc::new(RegistryConfig { 657 | auth_token: Some("SOME_VALUE".to_string()), 658 | ..Default::default() 659 | }) 660 | ),]) 661 | } 662 | ) 663 | } 664 | 665 | #[test] 666 | fn test_expand_vars() { 667 | assert_eq!( 668 | expand_vars("test${VAR}test", &|var_name| { 669 | match var_name { 670 | "VAR" => Some("VALUE".to_string()), 671 | _ => None, 672 | } 673 | }), 674 | "testVALUEtest" 675 | ); 676 | assert_eq!( 677 | expand_vars("${A}${B}${C}", &|var_name| { 678 | match var_name { 679 | "A" => Some("1".to_string()), 680 | "B" => Some("2".to_string()), 681 | "C" => Some("3".to_string()), 682 | _ => None, 683 | } 684 | }), 685 | "123" 686 | ); 687 | assert_eq!( 688 | expand_vars("test\\${VAR}test", &|var_name| { 689 | match var_name { 690 | "VAR" => Some("VALUE".to_string()), 691 | _ => None, 692 | } 693 | }), 694 | "test${VAR}test" 695 | ); 696 | assert_eq!( 697 | // npm ignores values with $ in them 698 | expand_vars("test${VA$R}test", &|_| { 699 | unreachable!(); 700 | }), 701 | "test${VA$R}test" 702 | ); 703 | assert_eq!( 704 | // npm ignores values with { in them 705 | expand_vars("test${VA{R}test", &|_| { 706 | unreachable!(); 707 | }), 708 | "test${VA{R}test" 709 | ); 710 | } 711 | 712 | #[test] 713 | fn test_scope_registry_url_only() { 714 | let npm_rc = NpmRc::parse( 715 | r#" 716 | @example:registry=https://example.com/ 717 | "#, 718 | &|_| None, 719 | ) 720 | .unwrap(); 721 | let npm_rc = npm_rc 722 | .as_resolved(&Url::parse("https://deno.land/npm/").unwrap()) 723 | .unwrap(); 724 | { 725 | let registry_url = npm_rc.get_registry_url("@example/test"); 726 | let config = npm_rc.get_registry_config("@example/test"); 727 | assert_eq!(registry_url.as_str(), "https://example.com/"); 728 | assert_eq!(config.as_ref(), &RegistryConfig::default()); 729 | } 730 | { 731 | let registry_url = npm_rc.get_registry_url("test"); 732 | let config = npm_rc.get_registry_config("test"); 733 | assert_eq!(registry_url.as_str(), "https://deno.land/npm/"); 734 | assert_eq!(config.as_ref(), &Default::default()); 735 | } 736 | } 737 | 738 | #[test] 739 | fn test_scope_with_auth() { 740 | let npm_rc = NpmRc::parse( 741 | r#" 742 | @example:registry=https://example.com/foo 743 | @example2:registry=https://example2.com/ 744 | //example.com/foo/:_authToken=MY_AUTH_TOKEN 745 | ; This one is borked - the URL must match registry URL exactly 746 | //example.com2/example/:_authToken=MY_AUTH_TOKEN2 747 | "#, 748 | &|_| None, 749 | ) 750 | .unwrap(); 751 | let npm_rc = npm_rc 752 | .as_resolved(&Url::parse("https://deno.land/npm/").unwrap()) 753 | .unwrap(); 754 | { 755 | let registry_url = npm_rc.get_registry_url("@example/test"); 756 | let config = npm_rc.get_registry_config("@example/test"); 757 | assert_eq!(registry_url.as_str(), "https://example.com/foo/"); 758 | assert_eq!( 759 | config.as_ref(), 760 | &RegistryConfig { 761 | auth_token: Some("MY_AUTH_TOKEN".to_string()), 762 | ..Default::default() 763 | } 764 | ); 765 | } 766 | { 767 | let registry_url = npm_rc.get_registry_url("@example2/test"); 768 | let config = npm_rc.get_registry_config("@example2/test"); 769 | assert_eq!(registry_url.as_str(), "https://example2.com/"); 770 | assert_eq!(config.as_ref(), &Default::default()); 771 | } 772 | } 773 | } 774 | -------------------------------------------------------------------------------- /src/registry.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. MIT license. 2 | 3 | use std::borrow::Cow; 4 | use std::cell::RefCell; 5 | use std::cmp::Ordering; 6 | use std::collections::HashMap; 7 | use std::rc::Rc; 8 | use std::sync::Arc; 9 | 10 | use async_trait::async_trait; 11 | use deno_semver::SmallStackString; 12 | use deno_semver::StackString; 13 | use deno_semver::Version; 14 | use deno_semver::VersionReq; 15 | use deno_semver::npm::NpmVersionReqParseError; 16 | use deno_semver::package::PackageName; 17 | use deno_semver::package::PackageNv; 18 | use serde::Deserialize; 19 | use serde::Serialize; 20 | use thiserror::Error; 21 | 22 | use crate::resolution::NewestDependencyDate; 23 | use crate::resolution::NpmPackageVersionNotFound; 24 | 25 | // npm registry docs: https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md 26 | 27 | #[derive(Debug, Default, Deserialize, Serialize, Clone)] 28 | pub struct NpmPackageInfo { 29 | pub name: PackageName, 30 | pub versions: HashMap, 31 | #[serde(default, rename = "dist-tags")] 32 | pub dist_tags: HashMap, 33 | #[serde(default, skip_serializing_if = "HashMap::is_empty")] 34 | #[serde(deserialize_with = "deserializers::hashmap")] 35 | pub time: HashMap>, 36 | } 37 | 38 | impl NpmPackageInfo { 39 | pub fn version_info<'a>( 40 | &'a self, 41 | nv: &PackageNv, 42 | link_packages: &'a HashMap>, 43 | ) -> Result<&'a NpmPackageVersionInfo, NpmPackageVersionNotFound> { 44 | if let Some(packages) = link_packages.get(&nv.name) { 45 | for pkg in packages { 46 | if pkg.version == nv.version { 47 | return Ok(pkg); 48 | } 49 | } 50 | } 51 | match self.versions.get(&nv.version) { 52 | Some(version_info) => Ok(version_info), 53 | None => Err(NpmPackageVersionNotFound(nv.clone())), 54 | } 55 | } 56 | } 57 | 58 | /// An iterator over all the package versions that takes into account the 59 | /// linked packages and the newest dependency date. 60 | pub struct NpmPackageVersionInfosIterator<'a> { 61 | iterator: Box + 'a>, 62 | info: &'a NpmPackageInfo, 63 | newest_dependency_date: Option, 64 | } 65 | 66 | impl<'a> NpmPackageVersionInfosIterator<'a> { 67 | pub fn new( 68 | info: &'a NpmPackageInfo, 69 | link_packages: Option<&'a Vec>, 70 | newest_dependency_date: Option, 71 | ) -> Self { 72 | let iterator: Box + 'a> = 73 | match link_packages { 74 | Some(link_version_infos) => Box::new(link_version_infos.iter().chain( 75 | info.versions.values().filter(move |v| { 76 | // assumes the user won't have a large amount of linked versions 77 | !link_version_infos.iter().any(|l| l.version == v.version) 78 | }), 79 | )), 80 | None => Box::new(info.versions.values()), 81 | }; 82 | Self { 83 | iterator, 84 | newest_dependency_date, 85 | info, 86 | } 87 | } 88 | } 89 | 90 | impl<'a> Iterator for NpmPackageVersionInfosIterator<'a> { 91 | type Item = &'a NpmPackageVersionInfo; 92 | 93 | fn next(&mut self) -> Option { 94 | self.iterator.by_ref().find(|&next| { 95 | self 96 | .newest_dependency_date 97 | .and_then(|newest_dependency_date| { 98 | // assume versions not in the time hashmap are really old 99 | self 100 | .info 101 | .time 102 | .get(&next.version) 103 | .map(|publish_date| newest_dependency_date.matches(*publish_date)) 104 | }) 105 | .unwrap_or(true) 106 | }) 107 | } 108 | } 109 | 110 | #[derive(Debug, Clone, Error, deno_error::JsError)] 111 | #[class(type)] 112 | #[error( 113 | "Error in {parent_nv} parsing version requirement for dependency \"{key}\": \"{value}\"" 114 | )] 115 | pub struct NpmDependencyEntryError { 116 | /// Name and version of the package that has this dependency. 117 | pub parent_nv: PackageNv, 118 | /// Bare specifier. 119 | pub key: String, 120 | /// Value of the dependency. 121 | pub value: String, 122 | #[source] 123 | pub source: NpmDependencyEntryErrorSource, 124 | } 125 | 126 | #[derive(Debug, Clone, Error)] 127 | pub enum NpmDependencyEntryErrorSource { 128 | #[error(transparent)] 129 | NpmVersionReqParseError(#[from] NpmVersionReqParseError), 130 | #[error("Package specified a dependency outside of npm ({}). Deno does not install these for security reasons. The npm package should be improved to have all its dependencies on npm. 131 | 132 | To work around this, you can use a package.json and install the dependencies via `npm install`.", .specifier)] 133 | RemoteDependency { specifier: String }, 134 | } 135 | 136 | #[derive(Debug, Clone, Eq, PartialEq)] 137 | pub enum NpmDependencyEntryKind { 138 | Dep, 139 | Peer, 140 | OptionalPeer, 141 | } 142 | 143 | impl NpmDependencyEntryKind { 144 | pub fn is_optional_peer(&self) -> bool { 145 | matches!(self, NpmDependencyEntryKind::OptionalPeer) 146 | } 147 | } 148 | 149 | #[derive(Debug, Clone, Eq, PartialEq)] 150 | pub struct NpmDependencyEntry { 151 | pub kind: NpmDependencyEntryKind, 152 | pub bare_specifier: StackString, 153 | pub name: PackageName, 154 | pub version_req: VersionReq, 155 | /// When the dependency is also marked as a peer dependency, 156 | /// use this entry to resolve the dependency when it can't 157 | /// be resolved as a peer dependency. 158 | pub peer_dep_version_req: Option, 159 | } 160 | 161 | impl PartialOrd for NpmDependencyEntry { 162 | fn partial_cmp(&self, other: &Self) -> Option { 163 | Some(self.cmp(other)) 164 | } 165 | } 166 | 167 | impl Ord for NpmDependencyEntry { 168 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 169 | // sort the dependencies alphabetically by name then by version descending 170 | match self.name.cmp(&other.name) { 171 | // sort by newest to oldest 172 | Ordering::Equal => other 173 | .version_req 174 | .version_text() 175 | .cmp(self.version_req.version_text()), 176 | ordering => ordering, 177 | } 178 | } 179 | } 180 | 181 | #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] 182 | pub struct NpmPeerDependencyMeta { 183 | #[serde(default)] 184 | #[serde(deserialize_with = "deserializers::null_default")] 185 | pub optional: bool, 186 | } 187 | 188 | #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] 189 | #[serde(untagged)] 190 | pub enum NpmPackageVersionBinEntry { 191 | String(String), 192 | Map(HashMap), 193 | } 194 | 195 | #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] 196 | #[serde(rename_all = "camelCase")] 197 | pub struct NpmPackageVersionInfo { 198 | pub version: Version, 199 | #[serde(default, skip_serializing_if = "Option::is_none")] 200 | pub dist: Option, 201 | #[serde(default, skip_serializing_if = "Option::is_none")] 202 | pub bin: Option, 203 | // Bare specifier to version (ex. `"typescript": "^3.0.1") or possibly 204 | // package and version (ex. `"typescript-3.0.1": "npm:typescript@3.0.1"`). 205 | #[serde(default, skip_serializing_if = "HashMap::is_empty")] 206 | #[serde(deserialize_with = "deserializers::hashmap")] 207 | pub dependencies: HashMap, 208 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 209 | #[serde(deserialize_with = "deserializers::vector")] 210 | pub bundle_dependencies: Vec, 211 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 212 | #[serde(deserialize_with = "deserializers::vector")] 213 | pub bundled_dependencies: Vec, 214 | #[serde(default, skip_serializing_if = "HashMap::is_empty")] 215 | #[serde(deserialize_with = "deserializers::hashmap")] 216 | pub optional_dependencies: HashMap, 217 | #[serde(default, skip_serializing_if = "HashMap::is_empty")] 218 | #[serde(deserialize_with = "deserializers::hashmap")] 219 | pub peer_dependencies: HashMap, 220 | #[serde(default, skip_serializing_if = "HashMap::is_empty")] 221 | #[serde(deserialize_with = "deserializers::hashmap")] 222 | pub peer_dependencies_meta: HashMap, 223 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 224 | #[serde(deserialize_with = "deserializers::vector")] 225 | pub os: Vec, 226 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 227 | #[serde(deserialize_with = "deserializers::vector")] 228 | pub cpu: Vec, 229 | #[serde(default, skip_serializing_if = "HashMap::is_empty")] 230 | #[serde(deserialize_with = "deserializers::hashmap")] 231 | pub scripts: HashMap, 232 | #[serde(default, skip_serializing_if = "Option::is_none")] 233 | #[serde(deserialize_with = "deserializers::string")] 234 | pub deprecated: Option, 235 | } 236 | 237 | impl NpmPackageVersionInfo { 238 | /// Helper for getting the bundle dependencies. 239 | /// 240 | /// Unfortunately due to limitations in serde, it's not 241 | /// easy to have a way to deserialize an alias without it 242 | /// throwing when the data has both fields, so we store both 243 | /// on the struct. 244 | pub fn bundle_dependencies(&self) -> &[StackString] { 245 | if self.bundle_dependencies.is_empty() { 246 | // only use the alias if the main field is empty 247 | &self.bundled_dependencies 248 | } else { 249 | &self.bundle_dependencies 250 | } 251 | } 252 | 253 | pub fn dependencies_as_entries( 254 | &self, 255 | // name of the package used to improve error messages 256 | package_name: &str, 257 | ) -> Result, Box> { 258 | fn parse_dep_entry_inner( 259 | (key, value): (&StackString, &StackString), 260 | kind: NpmDependencyEntryKind, 261 | ) -> Result { 262 | let (name, version_req) = 263 | parse_dep_entry_name_and_raw_version(key, value)?; 264 | let version_req = VersionReq::parse_from_npm(version_req)?; 265 | Ok(NpmDependencyEntry { 266 | kind, 267 | bare_specifier: key.clone(), 268 | name: PackageName::from_str(name), 269 | version_req, 270 | peer_dep_version_req: None, 271 | }) 272 | } 273 | 274 | fn parse_dep_entry( 275 | parent_nv: (&str, &Version), 276 | key_value: (&StackString, &StackString), 277 | kind: NpmDependencyEntryKind, 278 | ) -> Result> { 279 | parse_dep_entry_inner(key_value, kind).map_err(|source| { 280 | Box::new(NpmDependencyEntryError { 281 | parent_nv: PackageNv { 282 | name: parent_nv.0.into(), 283 | version: parent_nv.1.clone(), 284 | }, 285 | key: key_value.0.to_string(), 286 | value: key_value.1.to_string(), 287 | source, 288 | }) 289 | }) 290 | } 291 | 292 | let normalized_dependencies = if self 293 | .optional_dependencies 294 | .keys() 295 | .all(|k| self.dependencies.contains_key(k)) 296 | && self.bundle_dependencies().is_empty() 297 | { 298 | Cow::Borrowed(&self.dependencies) 299 | } else { 300 | // Most package information has the optional dependencies duplicated 301 | // in the dependencies list, but some don't. In those cases, add 302 | // the optonal dependencies into the map of dependencies 303 | Cow::Owned( 304 | self 305 | .optional_dependencies 306 | .iter() 307 | // prefer what's in the dependencies map 308 | .chain(self.dependencies.iter()) 309 | // exclude bundle dependencies 310 | .filter(|(k, _)| !self.bundle_dependencies().iter().any(|b| b == *k)) 311 | .map(|(k, v)| (k.clone(), v.clone())) 312 | .collect(), 313 | ) 314 | }; 315 | 316 | let mut result = HashMap::with_capacity( 317 | normalized_dependencies.len() + self.peer_dependencies.len(), 318 | ); 319 | let nv = (package_name, &self.version); 320 | for entry in &self.peer_dependencies { 321 | let is_optional = self 322 | .peer_dependencies_meta 323 | .get(entry.0) 324 | .map(|d| d.optional) 325 | .unwrap_or(false); 326 | let kind = match is_optional { 327 | true => NpmDependencyEntryKind::OptionalPeer, 328 | false => NpmDependencyEntryKind::Peer, 329 | }; 330 | let entry = parse_dep_entry(nv, entry, kind)?; 331 | result.insert(entry.bare_specifier.clone(), entry); 332 | } 333 | for entry in normalized_dependencies.iter() { 334 | let entry = parse_dep_entry(nv, entry, NpmDependencyEntryKind::Dep)?; 335 | // people may define a dependency as a peer dependency as well, 336 | // so in those cases, attempt to resolve as a peer dependency, 337 | // but then use this dependency version requirement otherwise 338 | if let Some(peer_dep_entry) = result.get_mut(&entry.bare_specifier) { 339 | peer_dep_entry.peer_dep_version_req = Some(entry.version_req); 340 | } else { 341 | result.insert(entry.bare_specifier.clone(), entry); 342 | } 343 | } 344 | Ok(result.into_values().collect()) 345 | } 346 | } 347 | 348 | /// Gets the name and raw version constraint for a registry info or 349 | /// package.json dependency entry taking into account npm package aliases. 350 | fn parse_dep_entry_name_and_raw_version<'a>( 351 | key: &'a str, 352 | value: &'a str, 353 | ) -> Result<(&'a str, &'a str), NpmDependencyEntryErrorSource> { 354 | let (name, version_req) = 355 | if let Some(package_and_version) = value.strip_prefix("npm:") { 356 | if let Some((name, version)) = package_and_version.rsplit_once('@') { 357 | // if empty, then the name was scoped and there's no version 358 | if name.is_empty() { 359 | (package_and_version, "*") 360 | } else { 361 | (name, version) 362 | } 363 | } else { 364 | (package_and_version, "*") 365 | } 366 | } else { 367 | (key, value) 368 | }; 369 | if version_req.starts_with("https://") 370 | || version_req.starts_with("http://") 371 | || version_req.starts_with("git:") 372 | || version_req.starts_with("github:") 373 | || version_req.starts_with("git+") 374 | { 375 | Err(NpmDependencyEntryErrorSource::RemoteDependency { 376 | specifier: version_req.to_string(), 377 | }) 378 | } else { 379 | Ok((name, version_req)) 380 | } 381 | } 382 | 383 | #[derive(Debug, Clone, PartialEq, Eq)] 384 | pub enum NpmPackageVersionDistInfoIntegrity<'a> { 385 | /// A string in the form `sha1-` where the hash is base64 encoded. 386 | Integrity { 387 | algorithm: &'a str, 388 | base64_hash: &'a str, 389 | }, 390 | /// The integrity could not be determined because it did not contain a dash. 391 | UnknownIntegrity(&'a str), 392 | /// The legacy sha1 hex hash (ex. "62afbee2ffab5e0db139450767a6125cbea50fa2"). 393 | LegacySha1Hex(&'a str), 394 | /// No integrity was found. 395 | None, 396 | } 397 | 398 | impl NpmPackageVersionDistInfoIntegrity<'_> { 399 | pub fn for_lockfile(&self) -> Option> { 400 | match self { 401 | NpmPackageVersionDistInfoIntegrity::Integrity { 402 | algorithm, 403 | base64_hash, 404 | } => Some(Cow::Owned(format!("{}-{}", algorithm, base64_hash))), 405 | NpmPackageVersionDistInfoIntegrity::UnknownIntegrity(integrity) => { 406 | Some(Cow::Borrowed(integrity)) 407 | } 408 | NpmPackageVersionDistInfoIntegrity::LegacySha1Hex(hex) => { 409 | Some(Cow::Borrowed(hex)) 410 | } 411 | NpmPackageVersionDistInfoIntegrity::None => None, 412 | } 413 | } 414 | } 415 | 416 | #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] 417 | pub struct NpmPackageVersionDistInfo { 418 | /// URL to the tarball. 419 | pub tarball: String, 420 | #[serde(default, skip_serializing_if = "Option::is_none")] 421 | pub(crate) shasum: Option, 422 | #[serde(default, skip_serializing_if = "Option::is_none")] 423 | pub(crate) integrity: Option, 424 | } 425 | 426 | impl NpmPackageVersionDistInfo { 427 | pub fn integrity(&self) -> NpmPackageVersionDistInfoIntegrity<'_> { 428 | match &self.integrity { 429 | Some(integrity) => match integrity.split_once('-') { 430 | Some((algorithm, base64_hash)) => { 431 | NpmPackageVersionDistInfoIntegrity::Integrity { 432 | algorithm, 433 | base64_hash, 434 | } 435 | } 436 | None => NpmPackageVersionDistInfoIntegrity::UnknownIntegrity( 437 | integrity.as_str(), 438 | ), 439 | }, 440 | None => match &self.shasum { 441 | Some(shasum) => { 442 | NpmPackageVersionDistInfoIntegrity::LegacySha1Hex(shasum) 443 | } 444 | None => NpmPackageVersionDistInfoIntegrity::None, 445 | }, 446 | } 447 | } 448 | } 449 | 450 | /// Error that occurs when loading the package info from the npm registry fails. 451 | #[derive(Debug, Error, Clone, deno_error::JsError)] 452 | pub enum NpmRegistryPackageInfoLoadError { 453 | #[class(type)] 454 | #[error("npm package '{package_name}' does not exist.")] 455 | PackageNotExists { package_name: String }, 456 | #[class(inherit)] 457 | #[error(transparent)] 458 | LoadError(Arc), 459 | } 460 | 461 | /// A trait for getting package information from the npm registry. 462 | /// 463 | /// An implementer may want to override the default implementation of 464 | /// [`mark_force_reload`] method if it has a cache mechanism. 465 | #[async_trait(?Send)] 466 | pub trait NpmRegistryApi { 467 | /// Gets the package information from the npm registry. 468 | /// 469 | /// Note: The implementer should handle requests for the same npm 470 | /// package name concurrently and try not to make the same request 471 | /// to npm at the same time. 472 | async fn package_info( 473 | &self, 474 | name: &str, 475 | ) -> Result, NpmRegistryPackageInfoLoadError>; 476 | 477 | /// Marks that new requests for package information should retrieve it 478 | /// from the npm registry 479 | /// 480 | /// Returns true if both of the following conditions are met: 481 | /// - the implementer has a cache mechanism 482 | /// - "force reload" flag is successfully set for the first time 483 | fn mark_force_reload(&self) -> bool { 484 | false 485 | } 486 | } 487 | 488 | /// A simple in-memory implementation of the NpmRegistryApi 489 | /// that can be used for testing purposes. This does not use 490 | /// `#[cfg(test)]` because that is not supported across crates. 491 | /// 492 | /// Note: This test struct is not thread safe for setup 493 | /// purposes. Construct everything on the same thread. 494 | #[derive(Clone, Default, Debug)] 495 | pub struct TestNpmRegistryApi { 496 | package_infos: Rc>>>, 497 | } 498 | 499 | #[async_trait::async_trait(?Send)] 500 | impl deno_lockfile::NpmPackageInfoProvider for TestNpmRegistryApi { 501 | async fn get_npm_package_info( 502 | &self, 503 | values: &[PackageNv], 504 | ) -> Result< 505 | Vec, 506 | Box, 507 | > { 508 | let mut infos = Vec::new(); 509 | let linked_packages = HashMap::new(); 510 | for nv in values { 511 | let info = self 512 | .package_info(nv.name.as_str()) 513 | .await 514 | .map_err(|e| Box::new(e) as Box)?; 515 | let version_info = info.version_info(nv, &linked_packages).unwrap(); 516 | let lockfile_info = deno_lockfile::Lockfile5NpmInfo { 517 | tarball_url: version_info 518 | .dist 519 | .as_ref() 520 | .map(|dist| dist.tarball.clone()), 521 | optional_dependencies: Default::default(), 522 | cpu: version_info.cpu.iter().map(|s| s.to_string()).collect(), 523 | os: version_info.os.iter().map(|s| s.to_string()).collect(), 524 | deprecated: version_info.deprecated.is_some(), 525 | bin: version_info.bin.is_some(), 526 | scripts: version_info.scripts.contains_key("preinstall") 527 | || version_info.scripts.contains_key("install") 528 | || version_info.scripts.contains_key("postinstall"), 529 | optional_peers: version_info 530 | .peer_dependencies 531 | .iter() 532 | .filter_map(|(k, v)| { 533 | if version_info 534 | .peer_dependencies_meta 535 | .get(k) 536 | .is_some_and(|m| m.optional) 537 | { 538 | Some((k.to_string(), v.to_string())) 539 | } else { 540 | None 541 | } 542 | }) 543 | .collect(), 544 | }; 545 | infos.push(lockfile_info); 546 | } 547 | Ok(infos) 548 | } 549 | } 550 | 551 | impl TestNpmRegistryApi { 552 | pub fn add_package_info(&self, name: &str, info: NpmPackageInfo) { 553 | let previous = self 554 | .package_infos 555 | .borrow_mut() 556 | .insert(name.to_string(), Arc::new(info)); 557 | assert!(previous.is_none()); 558 | } 559 | 560 | pub fn ensure_package(&self, name: &str) { 561 | if !self.package_infos.borrow().contains_key(name) { 562 | self.add_package_info( 563 | name, 564 | NpmPackageInfo { 565 | name: name.into(), 566 | ..Default::default() 567 | }, 568 | ); 569 | } 570 | } 571 | 572 | pub fn with_package(&self, name: &str, f: impl FnOnce(&mut NpmPackageInfo)) { 573 | self.ensure_package(name); 574 | let mut infos = self.package_infos.borrow_mut(); 575 | let mut info = infos.get_mut(name).unwrap().as_ref().clone(); 576 | f(&mut info); 577 | infos.insert(name.to_string(), Arc::new(info)); 578 | } 579 | 580 | pub fn add_dist_tag(&self, package_name: &str, tag: &str, version: &str) { 581 | self.with_package(package_name, |package| { 582 | package 583 | .dist_tags 584 | .insert(tag.to_string(), Version::parse_from_npm(version).unwrap()); 585 | }) 586 | } 587 | 588 | pub fn ensure_package_version(&self, name: &str, version: &str) { 589 | self.ensure_package_version_with_integrity(name, version, None) 590 | } 591 | 592 | pub fn ensure_package_version_with_integrity( 593 | &self, 594 | name: &str, 595 | version: &str, 596 | integrity: Option<&str>, 597 | ) { 598 | self.ensure_package(name); 599 | self.with_package(name, |info| { 600 | let version = Version::parse_from_npm(version).unwrap(); 601 | if !info.versions.contains_key(&version) { 602 | info.versions.insert( 603 | version.clone(), 604 | NpmPackageVersionInfo { 605 | version, 606 | dist: Some(NpmPackageVersionDistInfo { 607 | integrity: integrity.map(|s| s.to_string()), 608 | ..Default::default() 609 | }), 610 | ..Default::default() 611 | }, 612 | ); 613 | } 614 | }) 615 | } 616 | 617 | pub fn with_version_info( 618 | &self, 619 | package: (&str, &str), 620 | f: impl FnOnce(&mut NpmPackageVersionInfo), 621 | ) { 622 | let (name, version) = package; 623 | self.ensure_package_version(name, version); 624 | self.with_package(name, |info| { 625 | let version = Version::parse_from_npm(version).unwrap(); 626 | let version_info = info.versions.get_mut(&version).unwrap(); 627 | f(version_info); 628 | }); 629 | } 630 | 631 | pub fn add_dependency(&self, package: (&str, &str), entry: (&str, &str)) { 632 | self.with_version_info(package, |version| { 633 | version.dependencies.insert(entry.0.into(), entry.1.into()); 634 | }) 635 | } 636 | 637 | pub fn add_bundle_dependency( 638 | &self, 639 | package: (&str, &str), 640 | entry: (&str, &str), 641 | ) { 642 | self.with_version_info(package, |version| { 643 | version.dependencies.insert(entry.0.into(), entry.1.into()); 644 | version.bundle_dependencies.push(entry.0.into()); 645 | }) 646 | } 647 | 648 | pub fn add_dep_and_optional_dep( 649 | &self, 650 | package: (&str, &str), 651 | entry: (&str, &str), 652 | ) { 653 | self.with_version_info(package, |version| { 654 | version.dependencies.insert(entry.0.into(), entry.1.into()); 655 | version 656 | .optional_dependencies 657 | .insert(entry.0.into(), entry.1.into()); 658 | }) 659 | } 660 | 661 | pub fn add_optional_dep(&self, package: (&str, &str), entry: (&str, &str)) { 662 | self.with_version_info(package, |version| { 663 | version 664 | .optional_dependencies 665 | .insert(entry.0.into(), entry.1.into()); 666 | }) 667 | } 668 | 669 | pub fn add_peer_dependency( 670 | &self, 671 | package: (&str, &str), 672 | entry: (&str, &str), 673 | ) { 674 | self.with_version_info(package, |version| { 675 | version 676 | .peer_dependencies 677 | .insert(entry.0.into(), entry.1.into()); 678 | }); 679 | } 680 | 681 | pub fn add_optional_peer_dependency( 682 | &self, 683 | package: (&str, &str), 684 | entry: (&str, &str), 685 | ) { 686 | self.with_version_info(package, |version| { 687 | version 688 | .peer_dependencies 689 | .insert(entry.0.into(), entry.1.into()); 690 | version 691 | .peer_dependencies_meta 692 | .insert(entry.0.into(), NpmPeerDependencyMeta { optional: true }); 693 | }); 694 | } 695 | } 696 | 697 | #[async_trait(?Send)] 698 | impl NpmRegistryApi for TestNpmRegistryApi { 699 | async fn package_info( 700 | &self, 701 | name: &str, 702 | ) -> Result, NpmRegistryPackageInfoLoadError> { 703 | let infos = self.package_infos.borrow(); 704 | Ok(infos.get(name).cloned().ok_or_else(|| { 705 | NpmRegistryPackageInfoLoadError::PackageNotExists { 706 | package_name: name.into(), 707 | } 708 | })?) 709 | } 710 | } 711 | 712 | mod deserializers { 713 | use std::collections::HashMap; 714 | use std::fmt; 715 | 716 | use serde::Deserialize; 717 | use serde::Deserializer; 718 | use serde::de; 719 | use serde::de::DeserializeOwned; 720 | use serde::de::MapAccess; 721 | use serde::de::SeqAccess; 722 | use serde::de::Visitor; 723 | 724 | /// Deserializes empty or null values to the default value (npm allows uploading 725 | /// `null` for values and serde doesn't automatically make that the default). 726 | /// 727 | /// Code from: https://github.com/serde-rs/serde/issues/1098#issuecomment-760711617 728 | pub fn null_default<'de, D, T>(deserializer: D) -> Result 729 | where 730 | T: Default + Deserialize<'de>, 731 | D: serde::Deserializer<'de>, 732 | { 733 | let opt = Option::deserialize(deserializer)?; 734 | Ok(opt.unwrap_or_default()) 735 | } 736 | 737 | pub fn hashmap<'de, K, V, D>( 738 | deserializer: D, 739 | ) -> Result, D::Error> 740 | where 741 | K: DeserializeOwned + Eq + std::hash::Hash, 742 | V: DeserializeOwned, 743 | D: Deserializer<'de>, 744 | { 745 | deserializer.deserialize_option(HashMapVisitor:: { 746 | marker: std::marker::PhantomData, 747 | }) 748 | } 749 | 750 | pub fn string<'de, D>(deserializer: D) -> Result, D::Error> 751 | where 752 | D: Deserializer<'de>, 753 | { 754 | deserializer.deserialize_option(OptionalStringVisitor) 755 | } 756 | 757 | pub fn vector<'de, T, D>(deserializer: D) -> Result, D::Error> 758 | where 759 | T: DeserializeOwned, 760 | D: Deserializer<'de>, 761 | { 762 | deserializer.deserialize_option(VectorVisitor:: { 763 | marker: std::marker::PhantomData, 764 | }) 765 | } 766 | 767 | struct HashMapVisitor { 768 | marker: std::marker::PhantomData HashMap>, 769 | } 770 | 771 | impl<'de, K, V> Visitor<'de> for HashMapVisitor 772 | where 773 | K: DeserializeOwned + Eq + std::hash::Hash, 774 | V: DeserializeOwned, 775 | { 776 | type Value = HashMap; 777 | 778 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 779 | formatter.write_str("a map") 780 | } 781 | 782 | fn visit_none(self) -> Result 783 | where 784 | E: de::Error, 785 | { 786 | Ok(HashMap::new()) 787 | } 788 | 789 | fn visit_some(self, deserializer: D) -> Result 790 | where 791 | D: Deserializer<'de>, 792 | { 793 | deserializer.deserialize_any(self) 794 | } 795 | 796 | fn visit_map(self, mut map: M) -> Result 797 | where 798 | M: MapAccess<'de>, 799 | { 800 | let mut hashmap = HashMap::new(); 801 | 802 | // deserialize to a serde_json::Value first to ensure serde_json 803 | // skips over the entry, then deserialize to an actual value 804 | while let Some(entry) = 805 | map.next_entry::()? 806 | { 807 | if let Ok(key) = serde_json::from_value(entry.0) 808 | && let Ok(value) = serde_json::from_value(entry.1) 809 | { 810 | hashmap.insert(key, value); 811 | } 812 | } 813 | 814 | Ok(hashmap) 815 | } 816 | 817 | fn visit_seq(self, mut seq: A) -> Result 818 | where 819 | A: SeqAccess<'de>, 820 | { 821 | while seq.next_element::()?.is_some() {} 822 | Ok(HashMap::new()) 823 | } 824 | 825 | fn visit_bool(self, _v: bool) -> Result 826 | where 827 | E: de::Error, 828 | { 829 | Ok(HashMap::new()) 830 | } 831 | 832 | fn visit_i64(self, _v: i64) -> Result 833 | where 834 | E: de::Error, 835 | { 836 | Ok(HashMap::new()) 837 | } 838 | 839 | fn visit_u64(self, _v: u64) -> Result 840 | where 841 | E: de::Error, 842 | { 843 | Ok(HashMap::new()) 844 | } 845 | 846 | fn visit_f64(self, _v: f64) -> Result 847 | where 848 | E: de::Error, 849 | { 850 | Ok(HashMap::new()) 851 | } 852 | 853 | fn visit_string(self, _v: String) -> Result 854 | where 855 | E: de::Error, 856 | { 857 | Ok(HashMap::new()) 858 | } 859 | 860 | fn visit_str(self, _v: &str) -> Result 861 | where 862 | E: de::Error, 863 | { 864 | Ok(HashMap::new()) 865 | } 866 | } 867 | 868 | struct OptionalStringVisitor; 869 | 870 | impl<'de> Visitor<'de> for OptionalStringVisitor { 871 | type Value = Option; 872 | 873 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 874 | formatter.write_str("string or null") 875 | } 876 | 877 | fn visit_none(self) -> Result 878 | where 879 | E: de::Error, 880 | { 881 | Ok(None) 882 | } 883 | 884 | fn visit_map(self, mut map: M) -> Result 885 | where 886 | M: MapAccess<'de>, 887 | { 888 | while map 889 | .next_entry::()? 890 | .is_some() 891 | {} 892 | Ok(None) 893 | } 894 | 895 | fn visit_seq(self, mut seq: A) -> Result 896 | where 897 | A: SeqAccess<'de>, 898 | { 899 | while seq.next_element::()?.is_some() {} 900 | Ok(None) 901 | } 902 | 903 | fn visit_some(self, deserializer: D) -> Result 904 | where 905 | D: Deserializer<'de>, 906 | { 907 | deserializer.deserialize_any(self) 908 | } 909 | 910 | fn visit_bool(self, _v: bool) -> Result 911 | where 912 | E: de::Error, 913 | { 914 | Ok(None) 915 | } 916 | 917 | fn visit_i64(self, _v: i64) -> Result 918 | where 919 | E: de::Error, 920 | { 921 | Ok(None) 922 | } 923 | 924 | fn visit_u64(self, _v: u64) -> Result 925 | where 926 | E: de::Error, 927 | { 928 | Ok(None) 929 | } 930 | 931 | fn visit_f64(self, _v: f64) -> Result 932 | where 933 | E: de::Error, 934 | { 935 | Ok(None) 936 | } 937 | 938 | fn visit_string(self, v: String) -> Result 939 | where 940 | E: de::Error, 941 | { 942 | Ok(Some(v)) 943 | } 944 | 945 | fn visit_str(self, v: &str) -> Result 946 | where 947 | E: de::Error, 948 | { 949 | Ok(Some(v.to_string())) 950 | } 951 | 952 | fn visit_unit(self) -> Result 953 | where 954 | E: de::Error, 955 | { 956 | Ok(None) 957 | } 958 | } 959 | 960 | struct VectorVisitor { 961 | marker: std::marker::PhantomData Vec>, 962 | } 963 | 964 | impl<'de, T> Visitor<'de> for VectorVisitor 965 | where 966 | T: DeserializeOwned, 967 | { 968 | type Value = Vec; 969 | 970 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 971 | formatter.write_str("a sequence or null") 972 | } 973 | 974 | fn visit_none(self) -> Result 975 | where 976 | E: de::Error, 977 | { 978 | Ok(Vec::new()) 979 | } 980 | 981 | fn visit_some(self, deserializer: D) -> Result 982 | where 983 | D: Deserializer<'de>, 984 | { 985 | deserializer.deserialize_any(self) 986 | } 987 | 988 | fn visit_seq(self, mut seq: A) -> Result 989 | where 990 | A: SeqAccess<'de>, 991 | { 992 | let mut vec = Vec::new(); 993 | 994 | while let Some(value) = seq.next_element::()? { 995 | if let Ok(value) = serde_json::from_value(value) { 996 | vec.push(value); 997 | } 998 | } 999 | 1000 | Ok(vec) 1001 | } 1002 | 1003 | fn visit_map(self, mut map: M) -> Result 1004 | where 1005 | M: MapAccess<'de>, 1006 | { 1007 | while map 1008 | .next_entry::()? 1009 | .is_some() 1010 | {} 1011 | Ok(Vec::new()) 1012 | } 1013 | 1014 | fn visit_bool(self, _v: bool) -> Result 1015 | where 1016 | E: de::Error, 1017 | { 1018 | Ok(Vec::new()) 1019 | } 1020 | 1021 | fn visit_i64(self, _v: i64) -> Result 1022 | where 1023 | E: de::Error, 1024 | { 1025 | Ok(Vec::new()) 1026 | } 1027 | 1028 | fn visit_u64(self, _v: u64) -> Result 1029 | where 1030 | E: de::Error, 1031 | { 1032 | Ok(Vec::new()) 1033 | } 1034 | 1035 | fn visit_f64(self, _v: f64) -> Result 1036 | where 1037 | E: de::Error, 1038 | { 1039 | Ok(Vec::new()) 1040 | } 1041 | 1042 | fn visit_string(self, _v: String) -> Result 1043 | where 1044 | E: de::Error, 1045 | { 1046 | Ok(Vec::new()) 1047 | } 1048 | 1049 | fn visit_str(self, _v: &str) -> Result 1050 | where 1051 | E: de::Error, 1052 | { 1053 | Ok(Vec::new()) 1054 | } 1055 | 1056 | fn visit_unit(self) -> Result 1057 | where 1058 | E: de::Error, 1059 | { 1060 | Ok(Vec::new()) 1061 | } 1062 | } 1063 | } 1064 | 1065 | #[cfg(test)] 1066 | mod test { 1067 | use std::collections::HashMap; 1068 | 1069 | use deno_semver::Version; 1070 | use pretty_assertions::assert_eq; 1071 | use serde_json; 1072 | 1073 | use super::*; 1074 | 1075 | #[test] 1076 | fn deserializes_minimal_pkg_info() { 1077 | let text = r#"{ "version": "1.0.0", "dist": { "tarball": "value" } }"#; 1078 | let info: NpmPackageVersionInfo = serde_json::from_str(text).unwrap(); 1079 | assert_eq!( 1080 | info, 1081 | NpmPackageVersionInfo { 1082 | version: Version::parse_from_npm("1.0.0").unwrap(), 1083 | dist: Some(NpmPackageVersionDistInfo { 1084 | tarball: "value".to_string(), 1085 | shasum: None, 1086 | integrity: None, 1087 | }), 1088 | ..Default::default() 1089 | } 1090 | ); 1091 | } 1092 | 1093 | #[test] 1094 | fn deserializes_serializes_time() { 1095 | let text = r#"{ "name": "package", "versions": {}, "time": { "created": "2015-11-07T19:15:58.747Z", "1.0.0": "2015-11-07T19:15:58.747Z" } }"#; 1096 | let info: NpmPackageInfo = serde_json::from_str(text).unwrap(); 1097 | assert_eq!( 1098 | info.time, 1099 | HashMap::from([( 1100 | Version::parse_from_npm("1.0.0").unwrap(), 1101 | "2015-11-07T19:15:58.747Z".parse().unwrap(), 1102 | )]) 1103 | ); 1104 | assert_eq!( 1105 | serde_json::to_string(&info).unwrap(), 1106 | r#"{"name":"package","versions":{},"dist-tags":{},"time":{"1.0.0":"2015-11-07T19:15:58.747Z"}}"# 1107 | ); 1108 | } 1109 | 1110 | #[test] 1111 | fn deserializes_pkg_info_with_deprecated() { 1112 | let text = r#"{ 1113 | "version": "1.0.0", 1114 | "dist": { "tarball": "value", "shasum": "test" }, 1115 | "dependencies": ["key","value"], 1116 | "deprecated": "aa" 1117 | }"#; 1118 | let info: NpmPackageVersionInfo = serde_json::from_str(text).unwrap(); 1119 | assert_eq!( 1120 | info, 1121 | NpmPackageVersionInfo { 1122 | version: Version::parse_from_npm("1.0.0").unwrap(), 1123 | dist: Some(NpmPackageVersionDistInfo { 1124 | tarball: "value".to_string(), 1125 | shasum: Some("test".to_string()), 1126 | integrity: None, 1127 | }), 1128 | dependencies: HashMap::new(), 1129 | deprecated: Some("aa".to_string()), 1130 | ..Default::default() 1131 | } 1132 | ); 1133 | } 1134 | 1135 | #[test] 1136 | fn deserializes_pkg_info_with_deprecated_invalid() { 1137 | let values = [ 1138 | r#"["aa"]"#, 1139 | r#"{ "prop": "aa" }"#, 1140 | "1", 1141 | "1.0", 1142 | "true", 1143 | "null", 1144 | ]; 1145 | for value in values { 1146 | let text = format!( 1147 | r#"{{ 1148 | "version": "1.0.0", 1149 | "dist": {{ "tarball": "value", "shasum": "test" }}, 1150 | "dependencies": ["key","value"], 1151 | "deprecated": {} 1152 | }}"#, 1153 | value 1154 | ); 1155 | let info: NpmPackageVersionInfo = serde_json::from_str(&text).unwrap(); 1156 | assert_eq!( 1157 | info, 1158 | NpmPackageVersionInfo { 1159 | version: Version::parse_from_npm("1.0.0").unwrap(), 1160 | dist: Some(NpmPackageVersionDistInfo { 1161 | tarball: "value".to_string(), 1162 | shasum: Some("test".to_string()), 1163 | integrity: None, 1164 | }), 1165 | dependencies: HashMap::new(), 1166 | deprecated: None, 1167 | ..Default::default() 1168 | } 1169 | ); 1170 | } 1171 | } 1172 | 1173 | #[test] 1174 | fn deserializes_bin_entry() { 1175 | // string 1176 | let text = r#"{ "version": "1.0.0", "bin": "bin-value", "dist": { "tarball": "value", "shasum": "test" } }"#; 1177 | let info: NpmPackageVersionInfo = serde_json::from_str(text).unwrap(); 1178 | assert_eq!( 1179 | info.bin, 1180 | Some(NpmPackageVersionBinEntry::String("bin-value".to_string())) 1181 | ); 1182 | 1183 | // map 1184 | let text = r#"{ "version": "1.0.0", "bin": { "a": "a-value", "b": "b-value" }, "dist": { "tarball": "value", "shasum": "test" } }"#; 1185 | let info: NpmPackageVersionInfo = serde_json::from_str(text).unwrap(); 1186 | assert_eq!( 1187 | info.bin, 1188 | Some(NpmPackageVersionBinEntry::Map(HashMap::from([ 1189 | ("a".to_string(), "a-value".to_string()), 1190 | ("b".to_string(), "b-value".to_string()), 1191 | ]))) 1192 | ); 1193 | } 1194 | 1195 | #[test] 1196 | fn deserializes_null_entries() { 1197 | let text = r#"{ "version": "1.0.0", "dist": { "tarball": "value", "shasum": "test" }, "dependencies": null, "optionalDependencies": null, "peerDependencies": null, "peerDependenciesMeta": null, "os": null, "cpu": null, "scripts": null }"#; 1198 | let info: NpmPackageVersionInfo = serde_json::from_str(text).unwrap(); 1199 | assert!(info.dependencies.is_empty()); 1200 | assert!(info.optional_dependencies.is_empty()); 1201 | assert!(info.peer_dependencies.is_empty()); 1202 | assert!(info.peer_dependencies_meta.is_empty()); 1203 | assert!(info.os.is_empty()); 1204 | assert!(info.cpu.is_empty()); 1205 | assert!(info.scripts.is_empty()); 1206 | } 1207 | 1208 | #[test] 1209 | fn deserializes_bundle_dependencies_aliases() { 1210 | let text = r#"{ 1211 | "version": "1.0.0", 1212 | "dist": { "tarball": "value", "shasum": "test" }, 1213 | "bundleDependencies": ["a", "b"], 1214 | "bundledDependencies": ["b", "c"] 1215 | }"#; 1216 | let info: NpmPackageVersionInfo = serde_json::from_str(text).unwrap(); 1217 | let combined: Vec = info 1218 | .bundle_dependencies 1219 | .iter() 1220 | .chain(info.bundled_dependencies.iter()) 1221 | .map(|s| s.to_string()) 1222 | .collect(); 1223 | assert_eq!( 1224 | combined, 1225 | Vec::from([ 1226 | "a".to_string(), 1227 | "b".to_string(), 1228 | "b".to_string(), 1229 | "c".to_string(), 1230 | ]) 1231 | ); 1232 | assert_eq!( 1233 | info.bundle_dependencies(), 1234 | Vec::from(["a".to_string(), "b".to_string()]) 1235 | ); 1236 | } 1237 | 1238 | #[test] 1239 | fn deserializes_invalid_kind() { 1240 | #[track_caller] 1241 | fn assert_empty(text: &str) { 1242 | let info: NpmPackageVersionInfo = serde_json::from_str(text).unwrap(); 1243 | assert!(info.dependencies.is_empty()); 1244 | assert!(info.optional_dependencies.is_empty()); 1245 | assert!(info.peer_dependencies.is_empty()); 1246 | assert!(info.peer_dependencies_meta.is_empty()); 1247 | assert!(info.os.is_empty()); 1248 | assert!(info.cpu.is_empty()); 1249 | assert!(info.scripts.is_empty()); 1250 | } 1251 | 1252 | // wrong collection kind 1253 | assert_empty( 1254 | r#"{ 1255 | "version": "1.0.0", 1256 | "dist": { "tarball": "value", "shasum": "test" }, 1257 | "dependencies": [], 1258 | "optionalDependencies": [], 1259 | "peerDependencies": [], 1260 | "peerDependenciesMeta": [], 1261 | "os": {}, 1262 | "cpu": {}, 1263 | "scripts": [] 1264 | }"#, 1265 | ); 1266 | 1267 | // booleans 1268 | assert_empty( 1269 | r#"{ 1270 | "version": "1.0.0", 1271 | "dist": { "tarball": "value", "shasum": "test" }, 1272 | "dependencies": false, 1273 | "optionalDependencies": true, 1274 | "peerDependencies": false, 1275 | "peerDependenciesMeta": true, 1276 | "os": false, 1277 | "cpu": true, 1278 | "scripts": false 1279 | }"#, 1280 | ); 1281 | 1282 | // strings 1283 | assert_empty( 1284 | r#"{ 1285 | "version": "1.0.0", 1286 | "dist": { "tarball": "value", "shasum": "test" }, 1287 | "dependencies": "", 1288 | "optionalDependencies": "", 1289 | "peerDependencies": "", 1290 | "peerDependenciesMeta": "", 1291 | "os": "", 1292 | "cpu": "", 1293 | "scripts": "" 1294 | }"#, 1295 | ); 1296 | 1297 | // numbers 1298 | assert_empty( 1299 | r#"{ 1300 | "version": "1.0.0", 1301 | "dist": { "tarball": "value", "shasum": "test" }, 1302 | "dependencies": 1.23, 1303 | "optionalDependencies": 5, 1304 | "peerDependencies": -2, 1305 | "peerDependenciesMeta": -2.23, 1306 | "os": -63.34, 1307 | "cpu": 12, 1308 | "scripts": -1234.34 1309 | }"#, 1310 | ); 1311 | } 1312 | 1313 | #[test] 1314 | fn deserializes_invalid_collection_items() { 1315 | let text = r#"{ 1316 | "version": "1.0.0", 1317 | "dist": { "tarball": "value", "shasum": "test" }, 1318 | "dependencies": { 1319 | "value": 123, 1320 | "value1": 123.2, 1321 | "value2": -123, 1322 | "value3": -123.2, 1323 | "value4": true, 1324 | "value5": false, 1325 | "value6": null, 1326 | "value8": { 1327 | "value7": 123, 1328 | "value8": 123.2, 1329 | "value9": -123 1330 | }, 1331 | "value9": [ 1332 | 1, 1333 | 2, 1334 | 3 1335 | ], 1336 | "value10": "valid" 1337 | }, 1338 | "os": [ 1339 | 123, 1340 | 123.2, 1341 | -123, 1342 | -123.2, 1343 | true, 1344 | false, 1345 | null, 1346 | [1, 2, 3], 1347 | { 1348 | "prop": 2 1349 | }, 1350 | "valid" 1351 | ] 1352 | }"#; 1353 | let info: NpmPackageVersionInfo = serde_json::from_str(text).unwrap(); 1354 | assert_eq!( 1355 | info.dependencies, 1356 | HashMap::from([("value10".into(), "valid".into())]) 1357 | ); 1358 | assert_eq!(info.os, Vec::from(["valid".to_string()])); 1359 | } 1360 | 1361 | #[test] 1362 | fn itegrity() { 1363 | // integrity 1364 | let text = 1365 | r#"{ "tarball": "", "integrity": "sha512-testing", "shasum": "here" }"#; 1366 | let info: NpmPackageVersionDistInfo = serde_json::from_str(text).unwrap(); 1367 | assert_eq!( 1368 | info.integrity(), 1369 | super::NpmPackageVersionDistInfoIntegrity::Integrity { 1370 | algorithm: "sha512", 1371 | base64_hash: "testing" 1372 | } 1373 | ); 1374 | 1375 | // no integrity 1376 | let text = r#"{ "tarball": "", "shasum": "here" }"#; 1377 | let info: NpmPackageVersionDistInfo = serde_json::from_str(text).unwrap(); 1378 | assert_eq!( 1379 | info.integrity(), 1380 | super::NpmPackageVersionDistInfoIntegrity::LegacySha1Hex("here") 1381 | ); 1382 | 1383 | // no dash 1384 | let text = r#"{ "tarball": "", "integrity": "test", "shasum": "here" }"#; 1385 | let info: NpmPackageVersionDistInfo = serde_json::from_str(text).unwrap(); 1386 | assert_eq!( 1387 | info.integrity(), 1388 | super::NpmPackageVersionDistInfoIntegrity::UnknownIntegrity("test") 1389 | ); 1390 | } 1391 | 1392 | #[test] 1393 | fn test_parse_dep_entry_name_and_raw_version() { 1394 | let cases = [ 1395 | ("test", "^1.2", ("test", "^1.2")), 1396 | ("test", "1.x - 2.6", ("test", "1.x - 2.6")), 1397 | ("test", "npm:package@^1.2", ("package", "^1.2")), 1398 | ("test", "npm:package", ("package", "*")), 1399 | ("test", "npm:@scope/package", ("@scope/package", "*")), 1400 | ("test", "npm:@scope/package@1", ("@scope/package", "1")), 1401 | ]; 1402 | for (key, value, expected_result) in cases { 1403 | let key = StackString::from(key); 1404 | let value = StackString::from(value); 1405 | let (name, version) = 1406 | parse_dep_entry_name_and_raw_version(&key, &value).unwrap(); 1407 | assert_eq!((name, version), expected_result); 1408 | } 1409 | } 1410 | 1411 | #[test] 1412 | fn test_parse_dep_entry_name_and_raw_version_error() { 1413 | let err = parse_dep_entry_name_and_raw_version( 1414 | &StackString::from("test"), 1415 | &StackString::from("git:somerepo"), 1416 | ) 1417 | .unwrap_err(); 1418 | match err { 1419 | NpmDependencyEntryErrorSource::RemoteDependency { specifier } => { 1420 | assert_eq!(specifier, "git:somerepo") 1421 | } 1422 | _ => unreachable!(), 1423 | } 1424 | } 1425 | 1426 | #[test] 1427 | fn remote_deps_as_entries() { 1428 | for specifier in [ 1429 | "https://example.com/something.tgz", 1430 | "git://github.com/example/example", 1431 | "git+ssh://github.com/example/example", 1432 | ] { 1433 | let deps = NpmPackageVersionInfo { 1434 | dependencies: HashMap::from([("a".into(), specifier.into())]), 1435 | ..Default::default() 1436 | }; 1437 | let err = deps.dependencies_as_entries("pkg-name").unwrap_err(); 1438 | match err.source { 1439 | NpmDependencyEntryErrorSource::RemoteDependency { 1440 | specifier: err_specifier, 1441 | } => assert_eq!(err_specifier, specifier), 1442 | _ => unreachable!(), 1443 | } 1444 | } 1445 | } 1446 | 1447 | #[test] 1448 | fn example_deserialization_fail() { 1449 | #[derive(Debug, Serialize, Deserialize, Clone)] 1450 | pub struct SerializedCachedPackageInfo { 1451 | #[serde(flatten)] 1452 | pub info: NpmPackageInfo, 1453 | /// Custom property that includes the etag. 1454 | #[serde( 1455 | default, 1456 | skip_serializing_if = "Option::is_none", 1457 | rename = "_denoETag" 1458 | )] 1459 | pub etag: Option, 1460 | } 1461 | 1462 | let text = r#"{ 1463 | "name": "ts-morph", 1464 | "versions": { 1465 | "10.0.2": { 1466 | "version": "10.0.2", 1467 | "dist": { 1468 | "tarball": "https://registry.npmjs.org/ts-morph/-/ts-morph-10.0.2.tgz", 1469 | "shasum": "292418207db467326231b2be92828b5e295e7946", 1470 | "integrity": "sha512-TVuIfEqtr9dW25K3Jajqpqx7t/zLRFxKu2rXQZSDjTm4MO4lfmuj1hn8WEryjeDDBFcNOCi+yOmYUYR4HucrAg==" 1471 | }, 1472 | "bin": null, 1473 | "dependencies": { 1474 | "code-block-writer": "^10.1.1", 1475 | "@ts-morph/common": "~0.9.0" 1476 | }, 1477 | "deprecated": null 1478 | } 1479 | }, 1480 | "dist-tags": { "rc": "2.0.4-rc", "latest": "25.0.1" } 1481 | }"#; 1482 | let result = serde_json::from_str::(text); 1483 | assert!(result.is_ok()); 1484 | } 1485 | 1486 | #[test] 1487 | fn minimize_serialization_version_info() { 1488 | let data = NpmPackageVersionInfo { 1489 | version: Version::parse_from_npm("1.0.0").unwrap(), 1490 | dist: Default::default(), 1491 | bin: Default::default(), 1492 | dependencies: Default::default(), 1493 | bundle_dependencies: Default::default(), 1494 | bundled_dependencies: Default::default(), 1495 | optional_dependencies: Default::default(), 1496 | peer_dependencies: Default::default(), 1497 | peer_dependencies_meta: Default::default(), 1498 | os: Default::default(), 1499 | cpu: Default::default(), 1500 | scripts: Default::default(), 1501 | deprecated: Default::default(), 1502 | }; 1503 | let text = serde_json::to_string(&data).unwrap(); 1504 | assert_eq!(text, r#"{"version":"1.0.0"}"#); 1505 | } 1506 | 1507 | #[test] 1508 | fn minimize_serialization_dist() { 1509 | let data = NpmPackageVersionDistInfo { 1510 | tarball: "test".to_string(), 1511 | shasum: None, 1512 | integrity: None, 1513 | }; 1514 | let text = serde_json::to_string(&data).unwrap(); 1515 | assert_eq!(text, r#"{"tarball":"test"}"#); 1516 | } 1517 | } 1518 | --------------------------------------------------------------------------------