diff --git a/package.json b/package.json index a658c95d..0965a434 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@yarnpkg/monorepo", - "packageManager": "yarn@6.0.0-rc.19", + "packageManager": "yarn@6.0.0-git.20260814.hash-4421e301548e85d07d894540f89adc1345b1374a", "scripts": { "codegen:daemon-types": "cargo run -p zpm --bin generate_daemon_types", "codegen:daemon-types:check": "cargo run -p zpm --bin generate_daemon_types -- --check", diff --git a/packages/zpm-config/build.rs b/packages/zpm-config/build.rs index 1cd0cdbd..6a9fd0e5 100644 --- a/packages/zpm-config/build.rs +++ b/packages/zpm-config/build.rs @@ -105,6 +105,9 @@ struct Field { title: Option, description: Option, default: Option, + /// Set on array fields that also accept a single item, which then gets + /// treated as a list of one (`supportedArchitectures`, for instance). + one_or_many: Option, property_aliases: Option>>, properties: Option>, additional_keys: Option>, @@ -162,6 +165,7 @@ impl Field { type_: field.get_type(), aliases: field_aliases, default: field_default, + one_or_many: field.one_or_many.unwrap_or(false), }); } @@ -258,6 +262,7 @@ struct GeneratorField { type_: InternalType, aliases: Vec, default: String, + one_or_many: bool, } struct Generator { @@ -300,7 +305,13 @@ impl Generator { writeln!(writer, " #[serde(alias = \"{alias_camel_case}\")]").unwrap(); } - writeln!(writer, " #[serde(default)] pub {lc_snake_name}: Partial<{}>,", type_.to_intermediate_type_string()).unwrap(); + let deserialize_with = if field.one_or_many { + ", deserialize_with = \"crate::deserialize_one_or_many\"" + } else { + "" + }; + + writeln!(writer, " #[serde(default{deserialize_with})] pub {lc_snake_name}: Partial<{}>,", type_.to_intermediate_type_string()).unwrap(); } writeln!(writer, " }}").unwrap(); @@ -412,8 +423,17 @@ impl Generator { let lc_snake_name = name.to_case(Case::Snake); + // A one-or-many field is a single logical value, so the project + // configuration must replace the user one rather than extend it + // like regular list settings do. + let user_expr = if field.one_or_many { + format!("if let Partial::Value(_) = &project.{lc_snake_name} {{ Partial::Missing }} else {{ user.{lc_snake_name} }}") + } else { + format!("user.{lc_snake_name}") + }; + let merge_expr - = format!("MergeSettings::merge(context, user.{lc_snake_name}, project.{lc_snake_name}, {default})"); + = format!("MergeSettings::merge(context, {user_expr}, project.{lc_snake_name}, {default})"); if struct_name == &self.root_name { writeln!(writer, " {lc_snake_name}: {{").unwrap(); diff --git a/packages/zpm-config/schema.json b/packages/zpm-config/schema.json index 36a1a7a2..64992a3f 100644 --- a/packages/zpm-config/schema.json +++ b/packages/zpm-config/schema.json @@ -628,29 +628,27 @@ "default": 5000 }, "supportedArchitectures": { - "type": "object", - "title": "SupportedArchitectures", - "description": "The list of architectures we need to download in the cache.", - "properties": { - "cpu": { - "type": "array", - "description": "List of CPU architectures to cover.", - "items": { - "type": "zpm_utils::Cpu" - } - }, - "libc": { - "type": "array", - "description": "The list of standard C libraries to cover.", - "items": { - "type": "zpm_utils::Libc" - } - }, - "os": { - "type": "array", - "description": "The list of operating systems to cover.", - "items": { - "type": "zpm_utils::Os" + "type": "array", + "oneOrMany": true, + "description": "The list of architectures we need to download in the cache. Can either be a single entry (whose fields are combined together as a cross product) or a list of entries (each entry being matched independently).", + "items": { + "type": "object", + "title": "SupportedArchitectures", + "properties": { + "cpu": { + "type": "crate::ArchitectureFilter", + "description": "The CPU architecture (or list of architectures) to cover, or null to cover them all.", + "default": "current" + }, + "libc": { + "type": "crate::ArchitectureFilter", + "description": "The standard C library (or list of libraries) to cover, or null to cover them all.", + "default": "current" + }, + "os": { + "type": "crate::ArchitectureFilter", + "description": "The operating system (or list of systems) to cover, or null to cover them all.", + "default": "current" } } } diff --git a/packages/zpm-config/src/lib.rs b/packages/zpm-config/src/lib.rs index 034965f8..af83ec41 100644 --- a/packages/zpm-config/src/lib.rs +++ b/packages/zpm-config/src/lib.rs @@ -1,7 +1,7 @@ use std::{cell::Cell, collections::{BTreeMap, BTreeSet}, fmt::Display, ops::Deref, sync::Arc, time::UNIX_EPOCH}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; -use zpm_utils::{AbstractValue, Container, Cpu, DataType, FromFileString, IoResultExt, LastModifiedAt, Libc, Os, Path, RawString, Serialized, System, ToFileString, ToHumanString, tree}; +use zpm_utils::{AbstractValue, Container, Cpu, DataType, FromFileString, IoResultExt, LastModifiedAt, Libc, Os, Path, RawString, Serialized, System, SystemSet, ToFileString, ToHumanString, tree}; #[derive(Debug, Clone)] pub struct ConfigurationContext { @@ -110,6 +110,57 @@ impl<'de, T: Deserialize<'de>> Deserialize<'de> for Partial { } } +/// Deserializes a list setting that also accepts a single item, which is then +/// treated as a list of one. Used by settings such as `supportedArchitectures`, +/// which historically only accepted a single entry. +fn deserialize_one_or_many<'de, D, T>(deserializer: D) -> Result>, D::Error> + where D: Deserializer<'de>, T: Deserialize<'de> +{ + struct OneOrManyVisitor { + marker: std::marker::PhantomData, + } + + impl<'de, T: Deserialize<'de>> de::Visitor<'de> for OneOrManyVisitor { + type Value = Partial>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a single entry or a list of entries") + } + + fn visit_unit(self) -> Result { + Ok(Partial::Value(Vec::new())) + } + + fn visit_none(self) -> Result { + Ok(Partial::Value(Vec::new())) + } + + fn visit_some>(self, deserializer: D) -> Result { + deserializer.deserialize_any(self) + } + + fn visit_seq>(self, mut seq: A) -> Result { + let mut values + = Vec::new(); + + while let Some(value) = seq.next_element::()? { + values.push(value); + } + + Ok(Partial::Value(values)) + } + + fn visit_map>(self, map: A) -> Result { + let value + = T::deserialize(de::value::MapAccessDeserializer::new(map))?; + + Ok(Partial::Value(vec![value])) + } + } + + deserializer.deserialize_any(OneOrManyVisitor {marker: std::marker::PhantomData}) +} + impl Partial where T: Default { pub fn unwrap_or_default(self) -> T { match self { @@ -814,6 +865,22 @@ impl SourceRule { } impl Settings { + /// The systems we need to download packages for. Each entry of + /// `supportedArchitectures` yields one set, and a package is kept as soon + /// as it's compatible with at least one of them. + pub fn supported_systems(&self) -> Vec { + // An empty list would mean "no architecture at all", which is never + // what the user wants; we fallback on the current architecture instead + // (which is also what happens when the setting isn't set at all). + if self.supported_architectures.is_empty() { + return vec![SystemSet::from_current()]; + } + + self.supported_architectures.iter() + .map(|entry| entry.to_system_set()) + .collect() + } + pub fn disable_age_gate(&mut self) { self.npm_minimal_age_gate.force(std::time::Duration::ZERO, Source::Cli); @@ -878,63 +945,36 @@ fn validate_intermediate_settings(settings: &intermediate::Settings) -> Result<( Ok(()) } -impl SupportedArchitectures { - pub fn to_systems(&self) -> Vec { - let mut systems - = Vec::new(); - - let current - = System::from_current(); - - let cpus = if self.cpu.is_empty() { - vec![&Cpu::Current] - } else { - self.cpu.iter().map(|c| &c.value).collect() - }; - - let os = if self.os.is_empty() { - vec![&Os::Current] - } else { - self.os.iter().map(|o| &o.value).collect() - }; +/// Replaces the `current` placeholders by the values of the system we're +/// currently running on. Placeholders without a current value (the libc on +/// systems that don't have one, for instance) are simply removed. +fn resolve_current(values: &ArchitectureFilter, current: Option<&T>, placeholder: &T) -> Option> { + let values + = values.as_list()?; - let libc = if self.libc.is_empty() { - vec![&Libc::Current] + let resolved = values.iter() + .flat_map(|value| if value == placeholder { + current.cloned() } else { - self.libc.iter().map(|l| &l.value).collect() - }; - - for &cpu in &cpus { - for &os in &os { - for &libc in &libc { - let arch = if cpu == &Cpu::Current { - current.arch.clone() - } else { - Some(cpu.clone()) - }; + Some(value.clone()) + }) + .collect(); - let os = if os == &Os::Current { - current.os.clone() - } else { - Some(os.clone()) - }; + Some(resolved) +} - let libc = if libc == &Libc::Current { - current.libc.clone() - } else { - Some(libc.clone()) - }; +impl SupportedArchitectures { + /// The set of systems covered by this entry. Each field is matched + /// independently, so the entry covers the cross product of its fields. + pub fn to_system_set(&self) -> SystemSet { + let current + = System::from_current(); - systems.push(System { - arch, - os, - libc, - }); - } - } + SystemSet { + arch: resolve_current(&self.cpu.value, current.arch.as_ref(), &Cpu::Current), + os: resolve_current(&self.os.value, current.os.as_ref(), &Os::Current), + libc: resolve_current(&self.libc.value, current.libc.as_ref(), &Libc::Current), } - - systems } } @@ -1395,6 +1435,221 @@ pub use fns::*; mod types; pub use types::*; +#[cfg(test)] +mod tests { + use super::*; + + fn settings_from_yaml(text: &str) -> Settings { + let context = ConfigurationContext { + env: BTreeMap::new(), + user_cwd: None, + project_cwd: None, + package_cwd: None, + }; + + let project + = serde_yaml::from_str::(text) + .expect("The configuration should be valid"); + + Settings::merge( + &context, + Partial::Missing, + Partial::Value(project), + || panic!("No configuration found"), + ) + } + + fn settings_from_user_and_project_yaml(user_text: &str, project_text: &str) -> Settings { + let context = ConfigurationContext { + env: BTreeMap::new(), + user_cwd: None, + project_cwd: None, + package_cwd: None, + }; + + let user + = serde_yaml::from_str::(user_text) + .expect("The configuration should be valid"); + + let project + = serde_yaml::from_str::(project_text) + .expect("The configuration should be valid"); + + Settings::merge( + &context, + Partial::Value(user), + Partial::Value(project), + || panic!("No configuration found"), + ) + } + + fn supported_systems(text: &str) -> Vec { + settings_from_yaml(text).supported_systems() + } + + fn cpu(values: &[&str]) -> Option> { + Some(values.iter().map(|value| Cpu::from_file_string(value).unwrap()).collect()) + } + + fn os(values: &[&str]) -> Option> { + Some(values.iter().map(|value| Os::from_file_string(value).unwrap()).collect()) + } + + fn libc(values: &[&str]) -> Option> { + Some(values.iter().map(|value| Libc::from_file_string(value).unwrap()).collect()) + } + + #[test] + fn supported_architectures_should_support_the_legacy_object_form() { + let sets = supported_systems(r#" + supportedArchitectures: + os: [darwin, linux] + cpu: [arm64, x64] + libc: [glibc] + "#); + + assert_eq!(sets, vec![SystemSet { + arch: cpu(&["arm64", "x64"]), + os: os(&["darwin", "linux"]), + libc: libc(&["glibc"]), + }]); + } + + #[test] + fn supported_architectures_should_support_a_list_of_entries() { + let sets = supported_systems(r#" + supportedArchitectures: + - os: darwin + cpu: arm64 + libc: musl + - os: linux + cpu: x64 + libc: glibc + "#); + + assert_eq!(sets, vec![SystemSet { + arch: cpu(&["arm64"]), + os: os(&["darwin"]), + libc: libc(&["musl"]), + }, SystemSet { + arch: cpu(&["x64"]), + os: os(&["linux"]), + libc: libc(&["glibc"]), + }]); + } + + #[test] + fn supported_architectures_should_default_the_fields_that_arent_set_on_an_entry() { + let sets = supported_systems(r#" + supportedArchitectures: + - os: linux + "#); + + let current + = System::from_current(); + + assert_eq!(sets.len(), 1); + assert_eq!(sets[0].os, os(&["linux"])); + assert_eq!(sets[0].arch, Some(current.arch.into_iter().collect::>())); + } + + #[test] + fn supported_architectures_should_preserve_null_fields_inside_an_entry() { + let sets = supported_systems(r#" + supportedArchitectures: + - os: foo + cpu: [x64, ia32] + libc: null + "#); + + assert_eq!(sets, vec![SystemSet { + arch: cpu(&["x64", "ia32"]), + os: os(&["foo"]), + libc: None, + }]); + } + + #[test] + fn supported_architectures_should_fallback_on_the_current_architecture_when_the_list_is_empty() { + let sets = supported_systems(r#" + supportedArchitectures: [] + "#); + + assert_eq!(sets, vec![SystemSet::from_current()]); + } + + #[test] + fn supported_architectures_should_fallback_on_the_current_architecture_when_unset() { + let sets = supported_systems(r#" + enableGlobalCache: true + "#); + + assert_eq!(sets, vec![SystemSet::from_current()]); + } + + #[test] + fn supported_architectures_entries_are_matched_independently() { + let sets = supported_systems(r#" + supportedArchitectures: + - os: foo + cpu: x64 + libc: glibc + - os: bar + cpu: ia32 + libc: musl + "#); + + let foo_x64 = System::new(cpu(&["x64"]).unwrap().pop(), os(&["foo"]).unwrap().pop(), None) + .to_requirements(); + let foo_ia32 = System::new(cpu(&["ia32"]).unwrap().pop(), os(&["foo"]).unwrap().pop(), None) + .to_requirements(); + + assert!(foo_x64.validate_any(&sets)); + + // The cross product of both entries would have allowed it, but each + // entry has to match on its own. + assert!(!foo_ia32.validate_any(&sets)); + } + + #[test] + fn supported_architectures_should_be_replaced_by_the_project_configuration() { + let settings = settings_from_user_and_project_yaml(r#" + supportedArchitectures: + os: [darwin] + cpu: [arm64] + "#, r#" + supportedArchitectures: + os: [linux] + cpu: [x64] + "#); + + // The project configuration must replace the user one, not extend it; + // otherwise a project couldn't narrow down a user-level architecture set. + let sets = settings.supported_systems(); + + assert_eq!(sets.len(), 1); + assert_eq!(sets[0].os, os(&["linux"])); + assert_eq!(sets[0].arch, cpu(&["x64"])); + } + + #[test] + fn supported_architectures_should_use_the_user_configuration_when_the_project_doesnt_set_it() { + let settings = settings_from_user_and_project_yaml(r#" + supportedArchitectures: + os: [darwin] + cpu: [arm64] + "#, r#" + enableTelemetry: false + "#); + + let sets = settings.supported_systems(); + + assert_eq!(sets.len(), 1); + assert_eq!(sets[0].os, os(&["darwin"])); + assert_eq!(sets[0].arch, cpu(&["arm64"])); + } +} + // Rust doesn't support specialization, so we can't have a blanket implementation for FromStr // and a different one for Option; instead we manually generate whatever we need. merge_settings!(std::time::Duration, |s: &str| FromFileString::from_file_string(s).unwrap()); @@ -1446,6 +1701,10 @@ merge_optional_settings!(zpm_utils::Libc); merge_optional_settings!(zpm_utils::Os); merge_optional_settings!(zpm_utils::Secret); +merge_settings!(crate::types::ArchitectureFilter, |s: &str| FromFileString::from_file_string(s).unwrap()); +merge_settings!(crate::types::ArchitectureFilter, |s: &str| FromFileString::from_file_string(s).unwrap()); +merge_settings!(crate::types::ArchitectureFilter, |s: &str| FromFileString::from_file_string(s).unwrap()); + merge_settings!(crate::types::NodeLinker, |s: &str| FromFileString::from_file_string(s).unwrap()); merge_settings!(crate::types::NodePackageMapType, |s: &str| FromFileString::from_file_string(s).unwrap()); merge_settings!(crate::types::IslandLinker, |s: &str| FromFileString::from_file_string(s).unwrap()); diff --git a/packages/zpm-config/src/types.rs b/packages/zpm-config/src/types.rs index a57c716e..bd0b04d8 100644 --- a/packages/zpm-config/src/types.rs +++ b/packages/zpm-config/src/types.rs @@ -1,6 +1,141 @@ +use std::fmt; +use std::marker::PhantomData; + +use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use zpm_macro_enum::zpm_enum; +use zpm_utils::{FromFileString, ToFileString, ToHumanString}; + +use crate::{ConfigurationError, Interpolated}; + +/// One field of a `supportedArchitectures` entry. It can be set to a single +/// value, to a list of values, or to `null` (in which case every value is +/// supported - as opposed to an empty list, which supports none). +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ArchitectureFilter { + Any, + List(Vec), +} + +impl ArchitectureFilter { + /// The supported values, or `None` if the filter accepts them all. + pub fn as_list(&self) -> Option<&[T]> { + match self { + ArchitectureFilter::Any => None, + ArchitectureFilter::List(values) => Some(values), + } + } +} + +impl Default for ArchitectureFilter { + fn default() -> Self { + ArchitectureFilter::List(Vec::new()) + } +} + +impl FromFileString for ArchitectureFilter { + type Error = ::Error; -use crate::ConfigurationError; + fn from_file_string(s: &str) -> Result { + if s == "null" { + return Ok(ArchitectureFilter::Any); + } + + if s.is_empty() { + return Ok(ArchitectureFilter::List(Vec::new())); + } + + let values = s.split(',') + .map(|segment| T::from_file_string(segment.trim())) + .collect::, _>>()?; + + Ok(ArchitectureFilter::List(values)) + } +} + +impl ToFileString for ArchitectureFilter { + fn to_file_string(&self) -> String { + match self { + ArchitectureFilter::Any => "null".to_string(), + ArchitectureFilter::List(values) => values.iter() + .map(|value| value.to_file_string()) + .collect::>() + .join(","), + } + } +} + +impl ToHumanString for ArchitectureFilter { + fn to_print_string(&self) -> String { + match self { + ArchitectureFilter::Any => "null".to_string(), + ArchitectureFilter::List(values) => values.iter() + .map(|value| value.to_print_string()) + .collect::>() + .join(", "), + } + } +} + +impl Serialize for ArchitectureFilter { + fn serialize(&self, serializer: S) -> Result { + match self { + ArchitectureFilter::Any => serializer.serialize_none(), + ArchitectureFilter::List(values) => values.serialize(serializer), + } + } +} + +struct ArchitectureFilterVisitor { + marker: PhantomData, +} + +impl<'de, T> de::Visitor<'de> for ArchitectureFilterVisitor + where T: FromFileString + Deserialize<'de>, ::Error: fmt::Display +{ + type Value = ArchitectureFilter; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string, a list of strings, or null") + } + + fn visit_unit(self) -> Result { + Ok(ArchitectureFilter::Any) + } + + fn visit_none(self) -> Result { + Ok(ArchitectureFilter::Any) + } + + fn visit_some>(self, deserializer: D) -> Result { + deserializer.deserialize_any(self) + } + + fn visit_str(self, value: &str) -> Result { + ArchitectureFilter::from_file_string(value) + .map_err(de::Error::custom) + } + + fn visit_seq>(self, mut seq: A) -> Result { + let mut values + = Vec::new(); + + // We go through `Interpolated` so that each item can reference + // environment variables, just like any other setting. + while let Some(value) = seq.next_element::>()? { + values.push(value.into_inner()); + } + + Ok(ArchitectureFilter::List(values)) + } +} + +impl<'de, T> Deserialize<'de> for ArchitectureFilter + where T: FromFileString + Deserialize<'de>, ::Error: fmt::Display +{ + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_any(ArchitectureFilterVisitor {marker: PhantomData}) + } +} #[zpm_enum(error = ConfigurationError, or_else = |s| Err(ConfigurationError::EnumError(s.to_string())))] #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/packages/zpm-semver/src/range.rs b/packages/zpm-semver/src/range.rs index a6e46332..dd0fc923 100644 --- a/packages/zpm-semver/src/range.rs +++ b/packages/zpm-semver/src/range.rs @@ -168,6 +168,16 @@ impl Range { } } + /// Whether the range is the wildcard `*` range. + /// + /// The npm resolver special-cases it: since `*` never matches a prerelease + /// (cf. `check`), packages that only ever published prereleases wouldn't be + /// installable at all, so `*` is allowed to fall back on them when nothing + /// else matches. + pub fn is_wildcard(&self) -> bool { + self.source.as_str() == "*" + } + pub fn check(&self, version: &Version) -> bool { let mut n = 0; diff --git a/packages/zpm-utils/src/system.rs b/packages/zpm-utils/src/system.rs index a887a285..c3382d0e 100644 --- a/packages/zpm-utils/src/system.rs +++ b/packages/zpm-utils/src/system.rs @@ -97,6 +97,31 @@ impl System { } } +/// A set of systems, described by one list of supported values per field. +/// The systems covered by the set are the cross product of all the fields; +/// a `None` field means that all values are supported, whereas an empty +/// list means that none are. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SystemSet { + pub arch: Option>, + pub os: Option>, + pub libc: Option>, +} + +impl SystemSet { + /// The set only containing the system we're currently running on. + pub fn from_current() -> Self { + let current + = System::from_current(); + + Self { + arch: Some(current.arch.into_iter().collect()), + os: Some(current.os.into_iter().collect()), + libc: Some(current.libc.into_iter().collect()), + } + } +} + impl ToFileString for System { fn to_file_string(&self) -> String { let mut segments @@ -252,28 +277,29 @@ impl Requirements { true } - pub fn validate_any(&self, info: &Vec) -> bool { - let is_arch_valid = self.arch.is_empty() || self.arch.iter() - .any(|requirement| info.iter().any(|system| system.arch.as_ref() == Some(requirement))); - - if !is_arch_valid { - return false; - } - - let is_os_valid = self.os.is_empty() || self.os.iter() - .any(|requirement| info.iter().any(|system| system.os.as_ref() == Some(requirement))); + /// Whether the requirements are satisfied by at least one of the given + /// sets. Each set is checked as a whole, so listing two sets is *not* + /// the same as merging their fields together into a single one. + pub fn validate_any(&self, sets: &[SystemSet]) -> bool { + sets.iter().any(|set| self.validate_set(set)) + } - if !is_os_valid { - return false; - } + pub fn validate_set(&self, set: &SystemSet) -> bool { + fn is_field_valid(requirements: &[T], supported: &Option>) -> bool { + if requirements.is_empty() { + return true; + } - let is_libc_valid = self.libc.is_empty() || self.libc.iter() - .any(|requirement| info.iter().any(|system| system.libc.as_ref() == Some(requirement))); + let Some(supported) = supported else { + return true; + }; - if !is_libc_valid { - return false; + requirements.iter() + .any(|requirement| supported.contains(requirement)) } - true + is_field_valid(&self.arch, &set.arch) + && is_field_valid(&self.os, &set.os) + && is_field_valid(&self.libc, &set.libc) } } diff --git a/packages/zpm/data/builtin-extensions.json b/packages/zpm/data/builtin-extensions.json index cc1e8004..36e2786d 100644 --- a/packages/zpm/data/builtin-extensions.json +++ b/packages/zpm/data/builtin-extensions.json @@ -1 +1 @@ -[["@tailwindcss/aspect-ratio@<0.2.1",{"peerDependencies":{"tailwindcss":"^2.0.2"}}],["@tailwindcss/line-clamp@<0.2.1",{"peerDependencies":{"tailwindcss":"^2.0.2"}}],["@fullhuman/postcss-purgecss@3.1.3 || 3.1.3-alpha.0",{"peerDependencies":{"postcss":"^8.0.0"}}],["@samverschueren/stream-to-observable@<0.3.1",{"peerDependenciesMeta":{"rxjs":{"optional":true},"zenObservable":{"optional":true}}}],["any-observable@<0.5.1",{"peerDependenciesMeta":{"rxjs":{"optional":true},"zenObservable":{"optional":true}}}],["@pm2/agent@<1.0.4",{"dependencies":{"debug":"*"}}],["debug@<4.2.0",{"peerDependenciesMeta":{"supports-color":{"optional":true}}}],["got@<11",{"dependencies":{"@types/responselike":"^1.0.0","@types/keyv":"^3.1.1"}}],["cacheable-lookup@<4.1.2",{"dependencies":{"@types/keyv":"^3.1.1"}}],["http-link-dataloader@*",{"peerDependencies":{"graphql":"^0.13.1 || ^14.0.0"}}],["typescript-language-server@*",{"dependencies":{"vscode-jsonrpc":"^5.0.1","vscode-languageserver-protocol":"^3.15.0"}}],["postcss-syntax@*",{"peerDependenciesMeta":{"postcss-html":{"optional":true},"postcss-jsx":{"optional":true},"postcss-less":{"optional":true},"postcss-markdown":{"optional":true},"postcss-scss":{"optional":true}}}],["jss-plugin-rule-value-function@<=10.1.1",{"dependencies":{"tiny-warning":"^1.0.2"}}],["ink-select-input@<4.1.0",{"peerDependencies":{"react":"^16.8.2"}}],["license-webpack-plugin@<2.3.18",{"peerDependenciesMeta":{"webpack":{"optional":true}}}],["snowpack@>=3.3.0",{"dependencies":{"node-gyp":"^7.1.0"}}],["promise-inflight@*",{"peerDependenciesMeta":{"bluebird":{"optional":true}}}],["reactcss@*",{"peerDependencies":{"react":"*"}}],["react-color@<=2.19.0",{"peerDependencies":{"react":"*"}}],["gatsby-plugin-i18n@*",{"dependencies":{"ramda":"^0.24.1"}}],["useragent@^2.0.0",{"dependencies":{"request":"^2.88.0","yamlparser":"0.0.x","semver":"5.5.x"}}],["@apollographql/apollo-tools@<=0.5.2",{"peerDependencies":{"graphql":"^14.2.1 || ^15.0.0"}}],["material-table@^2.0.0",{"dependencies":{"@babel/runtime":"^7.11.2"}}],["@babel/parser@*",{"dependencies":{"@babel/types":"^7.8.3"}}],["fork-ts-checker-webpack-plugin@<=6.3.4",{"peerDependencies":{"eslint":">= 6","typescript":">= 2.7","webpack":">= 4","vue-template-compiler":"*"},"peerDependenciesMeta":{"eslint":{"optional":true},"vue-template-compiler":{"optional":true}}}],["rc-animate@<=3.1.1",{"peerDependencies":{"react":">=16.9.0","react-dom":">=16.9.0"}}],["react-bootstrap-table2-paginator@*",{"dependencies":{"classnames":"^2.2.6"}}],["react-draggable@<=4.4.3",{"peerDependencies":{"react":">= 16.3.0","react-dom":">= 16.3.0"}}],["apollo-upload-client@<14",{"peerDependencies":{"graphql":"14 - 15"}}],["react-instantsearch-core@<=6.7.0",{"peerDependencies":{"algoliasearch":">= 3.1 < 5"}}],["react-instantsearch-dom@<=6.7.0",{"dependencies":{"react-fast-compare":"^3.0.0"}}],["ws@<7.2.1",{"peerDependencies":{"bufferutil":"^4.0.1","utf-8-validate":"^5.0.2"},"peerDependenciesMeta":{"bufferutil":{"optional":true},"utf-8-validate":{"optional":true}}}],["react-portal@<4.2.2",{"peerDependencies":{"react-dom":"^15.0.0-0 || ^16.0.0-0 || ^17.0.0-0"}}],["react-scripts@<=4.0.1",{"peerDependencies":{"react":"*"}}],["testcafe@<=1.10.1",{"dependencies":{"@babel/plugin-transform-for-of":"^7.12.1","@babel/runtime":"^7.12.5"}}],["testcafe-legacy-api@<=4.2.0",{"dependencies":{"testcafe-hammerhead":"^17.0.1","read-file-relative":"^1.2.0"}}],["@google-cloud/firestore@<=4.9.3",{"dependencies":{"protobufjs":"^6.8.6"}}],["gatsby-source-apiserver@*",{"dependencies":{"babel-polyfill":"^6.26.0"}}],["@webpack-cli/package-utils@<=1.0.1-alpha.4",{"dependencies":{"cross-spawn":"^7.0.3"}}],["gatsby-remark-prismjs@<3.3.28",{"dependencies":{"lodash":"^4"}}],["gatsby-plugin-favicon@*",{"peerDependencies":{"webpack":"*"}}],["gatsby-plugin-sharp@<=4.6.0-next.3",{"dependencies":{"debug":"^4.3.1"}}],["gatsby-react-router-scroll@<=5.6.0-next.0",{"dependencies":{"prop-types":"^15.7.2"}}],["@rebass/forms@*",{"dependencies":{"@styled-system/should-forward-prop":"^5.0.0"},"peerDependencies":{"react":"^16.8.6"}}],["rebass@*",{"peerDependencies":{"react":"^16.8.6"}}],["@ant-design/react-slick@<=0.28.3",{"peerDependencies":{"react":">=16.0.0"}}],["mqtt@<4.2.7",{"dependencies":{"duplexify":"^4.1.1"}}],["vue-cli-plugin-vuetify@<=2.0.3",{"dependencies":{"semver":"^6.3.0"},"peerDependenciesMeta":{"sass-loader":{"optional":true},"vuetify-loader":{"optional":true}}}],["vue-cli-plugin-vuetify@<=2.0.4",{"dependencies":{"null-loader":"^3.0.0"}}],["vue-cli-plugin-vuetify@>=2.4.3",{"peerDependencies":{"vue":"*"}}],["@vuetify/cli-plugin-utils@<=0.0.4",{"dependencies":{"semver":"^6.3.0"},"peerDependenciesMeta":{"sass-loader":{"optional":true}}}],["@vue/cli-plugin-typescript@<=5.0.0-alpha.0",{"dependencies":{"babel-loader":"^8.1.0"}}],["@vue/cli-plugin-typescript@<=5.0.0-beta.0",{"dependencies":{"@babel/core":"^7.12.16"},"peerDependencies":{"vue-template-compiler":"^2.0.0"},"peerDependenciesMeta":{"vue-template-compiler":{"optional":true}}}],["cordova-ios@<=6.3.0",{"dependencies":{"underscore":"^1.9.2"}}],["cordova-lib@<=10.0.1",{"dependencies":{"underscore":"^1.9.2"}}],["git-node-fs@*",{"peerDependencies":{"js-git":"^0.7.8"},"peerDependenciesMeta":{"js-git":{"optional":true}}}],["consolidate@<0.16.0",{"peerDependencies":{"mustache":"^3.0.0"},"peerDependenciesMeta":{"mustache":{"optional":true}}}],["consolidate@<=0.16.0",{"peerDependencies":{"velocityjs":"^2.0.1","tinyliquid":"^0.2.34","liquid-node":"^3.0.1","jade":"^1.11.0","then-jade":"*","dust":"^0.3.0","dustjs-helpers":"^1.7.4","dustjs-linkedin":"^2.7.5","swig":"^1.4.2","swig-templates":"^2.0.3","razor-tmpl":"^1.3.1","atpl":">=0.7.6","liquor":"^0.0.5","twig":"^1.15.2","ejs":"^3.1.5","eco":"^1.1.0-rc-3","jazz":"^0.0.18","jqtpl":"~1.1.0","hamljs":"^0.6.2","hamlet":"^0.3.3","whiskers":"^0.4.0","haml-coffee":"^1.14.1","hogan.js":"^3.0.2","templayed":">=0.2.3","handlebars":"^4.7.6","underscore":"^1.11.0","lodash":"^4.17.20","pug":"^3.0.0","then-pug":"*","qejs":"^3.0.5","walrus":"^0.10.1","mustache":"^4.0.1","just":"^0.1.8","ect":"^0.5.9","mote":"^0.2.0","toffee":"^0.3.6","dot":"^1.1.3","bracket-template":"^1.1.5","ractive":"^1.3.12","nunjucks":"^3.2.2","htmling":"^0.0.8","babel-core":"^6.26.3","plates":"~0.4.11","react-dom":"^16.13.1","react":"^16.13.1","arc-templates":"^0.5.3","vash":"^0.13.0","slm":"^2.0.0","marko":"^3.14.4","teacup":"^2.0.0","coffee-script":"^1.12.7","squirrelly":"^5.1.0","twing":"^5.0.2"},"peerDependenciesMeta":{"velocityjs":{"optional":true},"tinyliquid":{"optional":true},"liquid-node":{"optional":true},"jade":{"optional":true},"then-jade":{"optional":true},"dust":{"optional":true},"dustjs-helpers":{"optional":true},"dustjs-linkedin":{"optional":true},"swig":{"optional":true},"swig-templates":{"optional":true},"razor-tmpl":{"optional":true},"atpl":{"optional":true},"liquor":{"optional":true},"twig":{"optional":true},"ejs":{"optional":true},"eco":{"optional":true},"jazz":{"optional":true},"jqtpl":{"optional":true},"hamljs":{"optional":true},"hamlet":{"optional":true},"whiskers":{"optional":true},"haml-coffee":{"optional":true},"hogan.js":{"optional":true},"templayed":{"optional":true},"handlebars":{"optional":true},"underscore":{"optional":true},"lodash":{"optional":true},"pug":{"optional":true},"then-pug":{"optional":true},"qejs":{"optional":true},"walrus":{"optional":true},"mustache":{"optional":true},"just":{"optional":true},"ect":{"optional":true},"mote":{"optional":true},"toffee":{"optional":true},"dot":{"optional":true},"bracket-template":{"optional":true},"ractive":{"optional":true},"nunjucks":{"optional":true},"htmling":{"optional":true},"babel-core":{"optional":true},"plates":{"optional":true},"react-dom":{"optional":true},"react":{"optional":true},"arc-templates":{"optional":true},"vash":{"optional":true},"slm":{"optional":true},"marko":{"optional":true},"teacup":{"optional":true},"coffee-script":{"optional":true},"squirrelly":{"optional":true},"twing":{"optional":true}}}],["vue-loader@<=16.3.3",{"peerDependencies":{"@vue/compiler-sfc":"^3.0.8","webpack":"^4.1.0 || ^5.0.0-0"},"peerDependenciesMeta":{"@vue/compiler-sfc":{"optional":true}}}],["vue-loader@^16.7.0",{"peerDependencies":{"@vue/compiler-sfc":"^3.0.8","vue":"^3.2.13"},"peerDependenciesMeta":{"@vue/compiler-sfc":{"optional":true},"vue":{"optional":true}}}],["scss-parser@<=1.0.5",{"dependencies":{"lodash":"^4.17.21"}}],["query-ast@<1.0.5",{"dependencies":{"lodash":"^4.17.21"}}],["redux-thunk@<=2.3.0",{"peerDependencies":{"redux":"^4.0.0"}}],["skypack@<=0.3.2",{"dependencies":{"tar":"^6.1.0"}}],["@npmcli/metavuln-calculator@<2.0.0",{"dependencies":{"json-parse-even-better-errors":"^2.3.1"}}],["bin-links@<2.3.0",{"dependencies":{"mkdirp-infer-owner":"^1.0.2"}}],["rollup-plugin-polyfill-node@<=0.8.0",{"peerDependencies":{"rollup":"^1.20.0 || ^2.0.0"}}],["snowpack@<3.8.6",{"dependencies":{"magic-string":"^0.25.7"}}],["elm-webpack-loader@*",{"dependencies":{"temp":"^0.9.4"}}],["winston-transport@<=4.4.0",{"dependencies":{"logform":"^2.2.0"}}],["jest-vue-preprocessor@*",{"dependencies":{"@babel/core":"7.8.7","@babel/template":"7.8.6"},"peerDependencies":{"pug":"^2.0.4"},"peerDependenciesMeta":{"pug":{"optional":true}}}],["redux-persist@*",{"peerDependencies":{"react":">=16"},"peerDependenciesMeta":{"react":{"optional":true}}}],["sodium@>=3",{"dependencies":{"node-gyp":"^3.8.0"}}],["babel-plugin-graphql-tag@<=3.1.0",{"peerDependencies":{"graphql":"^14.0.0 || ^15.0.0"}}],["@playwright/test@<=1.14.1",{"dependencies":{"jest-matcher-utils":"^26.4.2"}}],["babel-plugin-remove-graphql-queries@<3.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["babel-preset-gatsby-package@<1.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["create-gatsby@<1.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-admin@<0.24.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-cli@<3.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-core-utils@<2.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-design-tokens@<3.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-legacy-polyfills@<1.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-benchmark-reporting@<1.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-graphql-config@<0.23.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-image@<1.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-mdx@<2.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-netlify-cms@<5.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-no-sourcemaps@<3.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-page-creator@<3.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-preact@<5.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-preload-fonts@<2.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-schema-snapshot@<2.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-styletron@<6.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-subfont@<3.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-utils@<1.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-recipes@<0.25.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-source-shopify@<5.6.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-source-wikipedia@<3.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-transformer-screenshot@<3.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-worker@<0.5.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-core-utils@<2.14.0-next.1",{"dependencies":{"got":"8.3.2"}}],["gatsby-plugin-gatsby-cloud@<=3.1.0-next.0",{"dependencies":{"gatsby-core-utils":"^2.13.0-next.0"}}],["gatsby-plugin-gatsby-cloud@<=3.2.0-next.1",{"peerDependencies":{"webpack":"*"}}],["babel-plugin-remove-graphql-queries@<=3.14.0-next.1",{"dependencies":{"gatsby-core-utils":"^2.8.0-next.1"}}],["gatsby-plugin-netlify@3.13.0-next.1",{"dependencies":{"gatsby-core-utils":"^2.13.0-next.0"}}],["clipanion-v3-codemod@<=0.2.0",{"peerDependencies":{"jscodeshift":"^0.11.0"}}],["react-live@*",{"peerDependencies":{"react-dom":"*","react":"*"}}],["webpack@<4.44.1",{"peerDependenciesMeta":{"webpack-cli":{"optional":true},"webpack-command":{"optional":true}}}],["webpack@<5.0.0-beta.23",{"peerDependenciesMeta":{"webpack-cli":{"optional":true}}}],["webpack-dev-server@<3.10.2",{"peerDependenciesMeta":{"webpack-cli":{"optional":true}}}],["@docusaurus/responsive-loader@<1.5.0",{"peerDependenciesMeta":{"sharp":{"optional":true},"jimp":{"optional":true}}}],["eslint-module-utils@*",{"peerDependenciesMeta":{"eslint-import-resolver-node":{"optional":true},"eslint-import-resolver-typescript":{"optional":true},"eslint-import-resolver-webpack":{"optional":true},"@typescript-eslint/parser":{"optional":true}}}],["eslint-plugin-import@*",{"peerDependenciesMeta":{"@typescript-eslint/parser":{"optional":true}}}],["critters-webpack-plugin@<3.0.2",{"peerDependenciesMeta":{"html-webpack-plugin":{"optional":true}}}],["terser@<=5.10.0",{"dependencies":{"acorn":"^8.5.0"}}],["babel-preset-react-app@10.0.x <10.0.2",{"dependencies":{"@babel/plugin-proposal-private-property-in-object":"^7.16.7"}}],["eslint-config-react-app@*",{"peerDependenciesMeta":{"typescript":{"optional":true}}}],["@vue/eslint-config-typescript@<11.0.0",{"peerDependenciesMeta":{"typescript":{"optional":true}}}],["unplugin-vue2-script-setup@<0.9.1",{"peerDependencies":{"@vue/composition-api":"^1.4.3","@vue/runtime-dom":"^3.2.26"}}],["@cypress/snapshot@*",{"dependencies":{"debug":"^3.2.7"}}],["auto-relay@<=0.14.0",{"peerDependencies":{"reflect-metadata":"^0.1.13"}}],["vue-template-babel-compiler@<1.2.0",{"peerDependencies":{"vue-template-compiler":"^2.6.0"}}],["@parcel/transformer-image@<2.5.0",{"peerDependencies":{"@parcel/core":"*"}}],["@parcel/transformer-js@<2.5.0",{"peerDependencies":{"@parcel/core":"*"}}],["parcel@*",{"peerDependenciesMeta":{"@parcel/core":{"optional":true}}}],["react-scripts@*",{"peerDependencies":{"eslint":"*"}}],["focus-trap-react@^8.0.0",{"dependencies":{"tabbable":"^5.3.2"}}],["react-rnd@<10.3.7",{"peerDependencies":{"react":">=16.3.0","react-dom":">=16.3.0"}}],["connect-mongo@<5.0.0",{"peerDependencies":{"express-session":"^1.17.1"}}],["vue-i18n@<9",{"peerDependencies":{"vue":"^2"}}],["vue-router@<4",{"peerDependencies":{"vue":"^2"}}],["unified@<10",{"dependencies":{"@types/unist":"^2.0.0"}}],["react-github-btn@<=1.3.0",{"peerDependencies":{"react":">=16.3.0"}}],["react-dev-utils@*",{"peerDependencies":{"typescript":">=2.7","webpack":">=4"},"peerDependenciesMeta":{"typescript":{"optional":true}}}],["@asyncapi/react-component@<=1.0.0-next.39",{"peerDependencies":{"react":">=16.8.0","react-dom":">=16.8.0"}}],["xo@*",{"peerDependencies":{"webpack":">=1.11.0"},"peerDependenciesMeta":{"webpack":{"optional":true}}}],["babel-plugin-remove-graphql-queries@<=4.20.0-next.0",{"dependencies":{"@babel/types":"^7.15.4"}}],["gatsby-plugin-page-creator@<=4.20.0-next.1",{"dependencies":{"fs-extra":"^10.1.0"}}],["gatsby-plugin-utils@<=3.14.0-next.1",{"dependencies":{"fastq":"^1.13.0"},"peerDependencies":{"graphql":"^15.0.0"}}],["gatsby-plugin-mdx@<3.1.0-next.1",{"dependencies":{"mkdirp":"^1.0.4"}}],["gatsby-plugin-mdx@^2",{"peerDependencies":{"gatsby":"^3.0.0-next"}}],["fdir@<=5.2.0",{"peerDependencies":{"picomatch":"2.x"},"peerDependenciesMeta":{"picomatch":{"optional":true}}}],["babel-plugin-transform-typescript-metadata@<=0.3.2",{"peerDependencies":{"@babel/core":"^7","@babel/traverse":"^7"},"peerDependenciesMeta":{"@babel/traverse":{"optional":true}}}],["graphql-compose@>=9.0.10",{"peerDependencies":{"graphql":"^14.2.0 || ^15.0.0 || ^16.0.0"}}],["vite-plugin-vuetify@<=1.0.2",{"peerDependencies":{"vue":"^3.0.0"}}],["webpack-plugin-vuetify@<=2.0.1",{"peerDependencies":{"vue":"^3.2.6"}}],["eslint-import-resolver-vite@<2.0.1",{"dependencies":{"debug":"^4.3.4","resolve":"^1.22.8"}}],["notistack@^3.0.0",{"dependencies":{"csstype":"^3.0.10"}}],["@fastify/type-provider-typebox@^5.0.0",{"peerDependencies":{"fastify":"^5.0.0"}}],["@fastify/type-provider-typebox@^4.0.0",{"peerDependencies":{"fastify":"^4.0.0"}}]] \ No newline at end of file +[["@tailwindcss/aspect-ratio@<0.2.1",{"peerDependencies":{"tailwindcss":"^2.0.2"}}],["@tailwindcss/line-clamp@<0.2.1",{"peerDependencies":{"tailwindcss":"^2.0.2"}}],["@fullhuman/postcss-purgecss@3.1.3 || 3.1.3-alpha.0",{"peerDependencies":{"postcss":"^8.0.0"}}],["@samverschueren/stream-to-observable@<0.3.1",{"peerDependenciesMeta":{"rxjs":{"optional":true},"zenObservable":{"optional":true}}}],["any-observable@<0.5.1",{"peerDependenciesMeta":{"rxjs":{"optional":true},"zenObservable":{"optional":true}}}],["@pm2/agent@<1.0.4",{"dependencies":{"debug":"*"}}],["debug@<4.2.0",{"peerDependenciesMeta":{"supports-color":{"optional":true}}}],["got@<11",{"dependencies":{"@types/responselike":"^1.0.0","@types/keyv":"^3.1.1"}}],["cacheable-lookup@<4.1.2",{"dependencies":{"@types/keyv":"^3.1.1"}}],["http-link-dataloader@*",{"peerDependencies":{"graphql":"^0.13.1 || ^14.0.0"}}],["typescript-language-server@*",{"dependencies":{"vscode-jsonrpc":"^5.0.1","vscode-languageserver-protocol":"^3.15.0"}}],["postcss-syntax@*",{"peerDependenciesMeta":{"postcss-html":{"optional":true},"postcss-jsx":{"optional":true},"postcss-less":{"optional":true},"postcss-markdown":{"optional":true},"postcss-scss":{"optional":true}}}],["jss-plugin-rule-value-function@<=10.1.1",{"dependencies":{"tiny-warning":"^1.0.2"}}],["ink-select-input@<4.1.0",{"peerDependencies":{"react":"^16.8.2"}}],["license-webpack-plugin@<2.3.18",{"peerDependenciesMeta":{"webpack":{"optional":true}}}],["snowpack@>=3.3.0",{"dependencies":{"node-gyp":"^7.1.0"}}],["promise-inflight@*",{"peerDependenciesMeta":{"bluebird":{"optional":true}}}],["reactcss@*",{"peerDependencies":{"react":"*"}}],["react-color@<=2.19.0",{"peerDependencies":{"react":"*"}}],["gatsby-plugin-i18n@*",{"dependencies":{"ramda":"^0.24.1"}}],["useragent@^2.0.0",{"dependencies":{"request":"^2.88.0","yamlparser":"0.0.x","semver":"5.5.x"}}],["@apollographql/apollo-tools@<=0.5.2",{"peerDependencies":{"graphql":"^14.2.1 || ^15.0.0"}}],["material-table@^2.0.0",{"dependencies":{"@babel/runtime":"^7.11.2"}}],["@babel/parser@*",{"dependencies":{"@babel/types":"^7.8.3"}}],["fork-ts-checker-webpack-plugin@<=6.3.4",{"peerDependencies":{"eslint":">= 6","typescript":">= 2.7","webpack":">= 4","vue-template-compiler":"*"},"peerDependenciesMeta":{"eslint":{"optional":true},"vue-template-compiler":{"optional":true}}}],["rc-animate@<=3.1.1",{"peerDependencies":{"react":">=16.9.0","react-dom":">=16.9.0"}}],["react-bootstrap-table2-paginator@*",{"dependencies":{"classnames":"^2.2.6"}}],["react-draggable@<=4.4.3",{"peerDependencies":{"react":">= 16.3.0","react-dom":">= 16.3.0"}}],["apollo-upload-client@<14",{"peerDependencies":{"graphql":"14 - 15"}}],["react-instantsearch-core@<=6.7.0",{"peerDependencies":{"algoliasearch":">= 3.1 < 5"}}],["react-instantsearch-dom@<=6.7.0",{"dependencies":{"react-fast-compare":"^3.0.0"}}],["ws@<7.2.1",{"peerDependencies":{"bufferutil":"^4.0.1","utf-8-validate":"^5.0.2"},"peerDependenciesMeta":{"bufferutil":{"optional":true},"utf-8-validate":{"optional":true}}}],["react-portal@<4.2.2",{"peerDependencies":{"react-dom":"^15.0.0-0 || ^16.0.0-0 || ^17.0.0-0"}}],["react-scripts@<=4.0.1",{"peerDependencies":{"react":"*"}}],["testcafe@<=1.10.1",{"dependencies":{"@babel/plugin-transform-for-of":"^7.12.1","@babel/runtime":"^7.12.5"}}],["testcafe-legacy-api@<=4.2.0",{"dependencies":{"testcafe-hammerhead":"^17.0.1","read-file-relative":"^1.2.0"}}],["@google-cloud/firestore@<=4.9.3",{"dependencies":{"protobufjs":"^6.8.6"}}],["gatsby-source-apiserver@*",{"dependencies":{"babel-polyfill":"^6.26.0"}}],["@webpack-cli/package-utils@<=1.0.1-alpha.4",{"dependencies":{"cross-spawn":"^7.0.3"}}],["gatsby-remark-prismjs@<3.3.28",{"dependencies":{"lodash":"^4"}}],["gatsby-plugin-favicon@*",{"peerDependencies":{"webpack":"*"}}],["gatsby-plugin-sharp@<=4.6.0-next.3",{"dependencies":{"debug":"^4.3.1"}}],["gatsby-react-router-scroll@<=5.6.0-next.0",{"dependencies":{"prop-types":"^15.7.2"}}],["@rebass/forms@*",{"dependencies":{"@styled-system/should-forward-prop":"^5.0.0"},"peerDependencies":{"react":"^16.8.6"}}],["rebass@*",{"peerDependencies":{"react":"^16.8.6"}}],["@ant-design/react-slick@<=0.28.3",{"peerDependencies":{"react":">=16.0.0"}}],["mqtt@<4.2.7",{"dependencies":{"duplexify":"^4.1.1"}}],["vue-cli-plugin-vuetify@<=2.0.3",{"dependencies":{"semver":"^6.3.0"},"peerDependenciesMeta":{"sass-loader":{"optional":true},"vuetify-loader":{"optional":true}}}],["vue-cli-plugin-vuetify@<=2.0.4",{"dependencies":{"null-loader":"^3.0.0"}}],["vue-cli-plugin-vuetify@>=2.4.3",{"peerDependencies":{"vue":"*"}}],["@vuetify/cli-plugin-utils@<=0.0.4",{"dependencies":{"semver":"^6.3.0"},"peerDependenciesMeta":{"sass-loader":{"optional":true}}}],["@vue/cli-plugin-typescript@<=5.0.0-alpha.0",{"dependencies":{"babel-loader":"^8.1.0"}}],["@vue/cli-plugin-typescript@<=5.0.0-beta.0",{"dependencies":{"@babel/core":"^7.12.16"},"peerDependencies":{"vue-template-compiler":"^2.0.0"},"peerDependenciesMeta":{"vue-template-compiler":{"optional":true}}}],["cordova-ios@<=6.3.0",{"dependencies":{"underscore":"^1.9.2"}}],["cordova-lib@<=10.0.1",{"dependencies":{"underscore":"^1.9.2"}}],["git-node-fs@*",{"peerDependencies":{"js-git":"^0.7.8"},"peerDependenciesMeta":{"js-git":{"optional":true}}}],["consolidate@<0.16.0",{"peerDependencies":{"mustache":"^3.0.0"},"peerDependenciesMeta":{"mustache":{"optional":true}}}],["consolidate@<=0.16.0",{"peerDependencies":{"velocityjs":"^2.0.1","tinyliquid":"^0.2.34","liquid-node":"^3.0.1","jade":"^1.11.0","then-jade":"*","dust":"^0.3.0","dustjs-helpers":"^1.7.4","dustjs-linkedin":"^2.7.5","swig":"^1.4.2","swig-templates":"^2.0.3","razor-tmpl":"^1.3.1","atpl":">=0.7.6","liquor":"^0.0.5","twig":"^1.15.2","ejs":"^3.1.5","eco":"^1.1.0-rc-3","jazz":"^0.0.18","jqtpl":"~1.1.0","hamljs":"^0.6.2","hamlet":"^0.3.3","whiskers":"^0.4.0","haml-coffee":"^1.14.1","hogan.js":"^3.0.2","templayed":">=0.2.3","handlebars":"^4.7.6","underscore":"^1.11.0","lodash":"^4.17.20","pug":"^3.0.0","then-pug":"*","qejs":"^3.0.5","walrus":"^0.10.1","mustache":"^4.0.1","just":"^0.1.8","ect":"^0.5.9","mote":"^0.2.0","toffee":"^0.3.6","dot":"^1.1.3","bracket-template":"^1.1.5","ractive":"^1.3.12","nunjucks":"^3.2.2","htmling":"^0.0.8","babel-core":"^6.26.3","plates":"~0.4.11","react-dom":"^16.13.1","react":"^16.13.1","arc-templates":"^0.5.3","vash":"^0.13.0","slm":"^2.0.0","marko":"^3.14.4","teacup":"^2.0.0","coffee-script":"^1.12.7","squirrelly":"^5.1.0","twing":"^5.0.2"},"peerDependenciesMeta":{"velocityjs":{"optional":true},"tinyliquid":{"optional":true},"liquid-node":{"optional":true},"jade":{"optional":true},"then-jade":{"optional":true},"dust":{"optional":true},"dustjs-helpers":{"optional":true},"dustjs-linkedin":{"optional":true},"swig":{"optional":true},"swig-templates":{"optional":true},"razor-tmpl":{"optional":true},"atpl":{"optional":true},"liquor":{"optional":true},"twig":{"optional":true},"ejs":{"optional":true},"eco":{"optional":true},"jazz":{"optional":true},"jqtpl":{"optional":true},"hamljs":{"optional":true},"hamlet":{"optional":true},"whiskers":{"optional":true},"haml-coffee":{"optional":true},"hogan.js":{"optional":true},"templayed":{"optional":true},"handlebars":{"optional":true},"underscore":{"optional":true},"lodash":{"optional":true},"pug":{"optional":true},"then-pug":{"optional":true},"qejs":{"optional":true},"walrus":{"optional":true},"mustache":{"optional":true},"just":{"optional":true},"ect":{"optional":true},"mote":{"optional":true},"toffee":{"optional":true},"dot":{"optional":true},"bracket-template":{"optional":true},"ractive":{"optional":true},"nunjucks":{"optional":true},"htmling":{"optional":true},"babel-core":{"optional":true},"plates":{"optional":true},"react-dom":{"optional":true},"react":{"optional":true},"arc-templates":{"optional":true},"vash":{"optional":true},"slm":{"optional":true},"marko":{"optional":true},"teacup":{"optional":true},"coffee-script":{"optional":true},"squirrelly":{"optional":true},"twing":{"optional":true}}}],["vue-loader@<=16.3.3",{"peerDependencies":{"@vue/compiler-sfc":"^3.0.8","webpack":"^4.1.0 || ^5.0.0-0"},"peerDependenciesMeta":{"@vue/compiler-sfc":{"optional":true}}}],["vue-loader@^16.7.0",{"peerDependencies":{"@vue/compiler-sfc":"^3.0.8","vue":"^3.2.13"},"peerDependenciesMeta":{"@vue/compiler-sfc":{"optional":true},"vue":{"optional":true}}}],["scss-parser@<=1.0.5",{"dependencies":{"lodash":"^4.17.21"}}],["query-ast@<1.0.5",{"dependencies":{"lodash":"^4.17.21"}}],["redux-thunk@<=2.3.0",{"peerDependencies":{"redux":"^4.0.0"}}],["skypack@<=0.3.2",{"dependencies":{"tar":"^6.1.0"}}],["@npmcli/metavuln-calculator@<2.0.0",{"dependencies":{"json-parse-even-better-errors":"^2.3.1"}}],["bin-links@<2.3.0",{"dependencies":{"mkdirp-infer-owner":"^1.0.2"}}],["rollup-plugin-polyfill-node@<=0.8.0",{"peerDependencies":{"rollup":"^1.20.0 || ^2.0.0"}}],["snowpack@<3.8.6",{"dependencies":{"magic-string":"^0.25.7"}}],["elm-webpack-loader@*",{"dependencies":{"temp":"^0.9.4"}}],["winston-transport@<=4.4.0",{"dependencies":{"logform":"^2.2.0"}}],["jest-vue-preprocessor@*",{"dependencies":{"@babel/core":"7.8.7","@babel/template":"7.8.6"},"peerDependencies":{"pug":"^2.0.4"},"peerDependenciesMeta":{"pug":{"optional":true}}}],["redux-persist@*",{"peerDependencies":{"react":">=16"},"peerDependenciesMeta":{"react":{"optional":true}}}],["sodium@>=3",{"dependencies":{"node-gyp":"^3.8.0"}}],["babel-plugin-graphql-tag@<=3.1.0",{"peerDependencies":{"graphql":"^14.0.0 || ^15.0.0"}}],["@playwright/test@<=1.14.1",{"dependencies":{"jest-matcher-utils":"^26.4.2"}}],["babel-plugin-remove-graphql-queries@<3.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["babel-preset-gatsby-package@<1.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["create-gatsby@<1.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-admin@<0.24.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-cli@<3.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-core-utils@<2.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-design-tokens@<3.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-legacy-polyfills@<1.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-benchmark-reporting@<1.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-graphql-config@<0.23.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-image@<1.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-mdx@<2.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-netlify-cms@<5.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-no-sourcemaps@<3.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-page-creator@<3.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-preact@<5.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-preload-fonts@<2.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-schema-snapshot@<2.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-styletron@<6.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-subfont@<3.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-plugin-utils@<1.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-recipes@<0.25.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-source-shopify@<5.6.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-source-wikipedia@<3.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-transformer-screenshot@<3.14.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-worker@<0.5.0-next.1",{"dependencies":{"@babel/runtime":"^7.14.8"}}],["gatsby-core-utils@<2.14.0-next.1",{"dependencies":{"got":"8.3.2"}}],["gatsby-plugin-gatsby-cloud@<=3.1.0-next.0",{"dependencies":{"gatsby-core-utils":"^2.13.0-next.0"}}],["gatsby-plugin-gatsby-cloud@<=3.2.0-next.1",{"peerDependencies":{"webpack":"*"}}],["babel-plugin-remove-graphql-queries@<=3.14.0-next.1",{"dependencies":{"gatsby-core-utils":"^2.8.0-next.1"}}],["gatsby-plugin-netlify@3.13.0-next.1",{"dependencies":{"gatsby-core-utils":"^2.13.0-next.0"}}],["clipanion-v3-codemod@<=0.2.0",{"peerDependencies":{"jscodeshift":"^0.11.0"}}],["react-live@*",{"peerDependencies":{"react-dom":"*","react":"*"}}],["webpack@<4.44.1",{"peerDependenciesMeta":{"webpack-cli":{"optional":true},"webpack-command":{"optional":true}}}],["webpack@<5.0.0-beta.23",{"peerDependenciesMeta":{"webpack-cli":{"optional":true}}}],["webpack-dev-server@<3.10.2",{"peerDependenciesMeta":{"webpack-cli":{"optional":true}}}],["@docusaurus/responsive-loader@<1.5.0",{"peerDependenciesMeta":{"sharp":{"optional":true},"jimp":{"optional":true}}}],["eslint-module-utils@*",{"peerDependenciesMeta":{"eslint-import-resolver-node":{"optional":true},"eslint-import-resolver-typescript":{"optional":true},"eslint-import-resolver-webpack":{"optional":true},"@typescript-eslint/parser":{"optional":true}}}],["eslint-plugin-import@*",{"peerDependenciesMeta":{"@typescript-eslint/parser":{"optional":true}}}],["critters-webpack-plugin@<3.0.2",{"peerDependenciesMeta":{"html-webpack-plugin":{"optional":true}}}],["terser@<=5.10.0",{"dependencies":{"acorn":"^8.5.0"}}],["babel-preset-react-app@10.0.x <10.0.2",{"dependencies":{"@babel/plugin-proposal-private-property-in-object":"^7.16.7"}}],["eslint-config-react-app@*",{"peerDependenciesMeta":{"typescript":{"optional":true}}}],["@vue/eslint-config-typescript@<11.0.0",{"peerDependenciesMeta":{"typescript":{"optional":true}}}],["unplugin-vue2-script-setup@<0.9.1",{"peerDependencies":{"@vue/composition-api":"^1.4.3","@vue/runtime-dom":"^3.2.26"}}],["@cypress/snapshot@*",{"dependencies":{"debug":"^3.2.7"}}],["auto-relay@<=0.14.0",{"peerDependencies":{"reflect-metadata":"^0.1.13"}}],["vue-template-babel-compiler@<1.2.0",{"peerDependencies":{"vue-template-compiler":"^2.6.0"}}],["@parcel/transformer-image@<2.5.0",{"peerDependencies":{"@parcel/core":"*"}}],["@parcel/transformer-js@<2.5.0",{"peerDependencies":{"@parcel/core":"*"}}],["parcel@*",{"peerDependenciesMeta":{"@parcel/core":{"optional":true}}}],["react-scripts@*",{"peerDependencies":{"eslint":"*"}}],["focus-trap-react@^8.0.0",{"dependencies":{"tabbable":"^5.3.2"}}],["react-rnd@<10.3.7",{"peerDependencies":{"react":">=16.3.0","react-dom":">=16.3.0"}}],["connect-mongo@<5.0.0",{"peerDependencies":{"express-session":"^1.17.1"}}],["vue-i18n@<9",{"peerDependencies":{"vue":"^2"}}],["vue-router@<4",{"peerDependencies":{"vue":"^2"}}],["unified@<10",{"dependencies":{"@types/unist":"^2.0.0"}}],["react-github-btn@<=1.3.0",{"peerDependencies":{"react":">=16.3.0"}}],["react-dev-utils@*",{"peerDependencies":{"typescript":">=2.7","webpack":">=4"},"peerDependenciesMeta":{"typescript":{"optional":true}}}],["@asyncapi/react-component@<=1.0.0-next.39",{"peerDependencies":{"react":">=16.8.0","react-dom":">=16.8.0"}}],["xo@*",{"peerDependencies":{"webpack":">=1.11.0"},"peerDependenciesMeta":{"webpack":{"optional":true}}}],["babel-plugin-remove-graphql-queries@<=4.20.0-next.0",{"dependencies":{"@babel/types":"^7.15.4"}}],["gatsby-plugin-page-creator@<=4.20.0-next.1",{"dependencies":{"fs-extra":"^10.1.0"}}],["gatsby-plugin-utils@<=3.14.0-next.1",{"dependencies":{"fastq":"^1.13.0"},"peerDependencies":{"graphql":"^15.0.0"}}],["gatsby-plugin-mdx@<3.1.0-next.1",{"dependencies":{"mkdirp":"^1.0.4"}}],["gatsby-plugin-mdx@^2",{"peerDependencies":{"gatsby":"^3.0.0-next"}}],["fdir@<=5.2.0",{"peerDependencies":{"picomatch":"2.x"},"peerDependenciesMeta":{"picomatch":{"optional":true}}}],["babel-plugin-transform-typescript-metadata@<=0.3.2",{"peerDependencies":{"@babel/core":"^7","@babel/traverse":"^7"},"peerDependenciesMeta":{"@babel/traverse":{"optional":true}}}],["graphql-compose@>=9.0.10",{"peerDependencies":{"graphql":"^14.2.0 || ^15.0.0 || ^16.0.0"}}],["vite-plugin-vuetify@<=1.0.2",{"peerDependencies":{"vue":"^3.0.0"}}],["webpack-plugin-vuetify@<=2.0.1",{"peerDependencies":{"vue":"^3.2.6"}}],["eslint-import-resolver-vite@<2.0.1",{"dependencies":{"debug":"^4.3.4","resolve":"^1.22.8"}}],["notistack@^3.0.0",{"dependencies":{"csstype":"^3.0.10"}}],["@fastify/type-provider-typebox@^5.0.0",{"peerDependencies":{"fastify":"^5.0.0"}}],["@fastify/type-provider-typebox@^4.0.0",{"peerDependencies":{"fastify":"^4.0.0"}}],["vite-plugin-vue-devtools@>=7.4.3",{"peerDependencies":{"vue":"*"}}],["@parcel/resolver-default@>=2",{"peerDependencies":{"@parcel/core":"*"}}],["@parcel/node-resolver-core@>=2",{"peerDependencies":{"@parcel/core":"*"}}],["@volar/typescript@*",{"peerDependencies":{"typescript":"*"},"peerDependenciesMeta":{"typescript":{"optional":true}}}],["@volar/language-server@*",{"peerDependencies":{"typescript":"*"},"peerDependenciesMeta":{"typescript":{"optional":true}}}],["@volar/language-service@*",{"peerDependencies":{"typescript":"*"},"peerDependenciesMeta":{"typescript":{"optional":true}}}],["volar-service-typescript@*",{"peerDependencies":{"typescript":"*"},"peerDependenciesMeta":{"typescript":{"optional":true}}}],["volar-service-typescript-twoslash-queries@*",{"peerDependencies":{"typescript":"*"},"peerDependenciesMeta":{"typescript":{"optional":true}}}]] \ No newline at end of file diff --git a/packages/zpm/patches/resolve.brotli.dat b/packages/zpm/patches/resolve.brotli.dat index a5e4f2d0..46319f1f 100644 Binary files a/packages/zpm/patches/resolve.brotli.dat and b/packages/zpm/patches/resolve.brotli.dat differ diff --git a/packages/zpm/patches/typescript.brotli.dat b/packages/zpm/patches/typescript.brotli.dat index 6855da29..d760f80f 100644 Binary files a/packages/zpm/patches/typescript.brotli.dat and b/packages/zpm/patches/typescript.brotli.dat differ diff --git a/packages/zpm/src/algolia.rs b/packages/zpm/src/algolia.rs index ff758ee1..6b1b8a36 100644 --- a/packages/zpm/src/algolia.rs +++ b/packages/zpm/src/algolia.rs @@ -1,14 +1,24 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, sync::Arc, time::Duration}; use serde::{Deserialize, Serialize}; +use zpm_config::Configuration; use zpm_parsers::JsonDocument; use zpm_primitives::Ident; use zpm_utils::ToFileString; -use crate::{error::Error, http::HttpClient}; +use crate::{error::Error, http::HttpClient, report::{if_active_async, with_report, StreamReport, StreamReportConfig}}; const ALGOLIA_URL: &str = "https://OFCNCOG2CU.algolia.net/1/indexes/*/objects"; +/// Maximum amount of time we're willing to wait for Algolia to tell us whether +/// the packages we're adding ship their types through DefinitelyTyped. The +/// lookup is a nicety, so we cap it way below the global `httpTimeout` setting; +/// otherwise a network that silently drops the connection (a corporate proxy, +/// for instance) would stall `yarn add` for as long as the global timeout. +/// +/// See https://github.com/yarnpkg/berry/issues/7111 +const ALGOLIA_TIMEOUT: Duration = Duration::from_secs(10); + #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct AlgoliaInputPayload { @@ -44,7 +54,51 @@ struct AlgoliaTypes { definitely_typed: Option, } -pub async fn query_algolia(idents: &[Ident], http_client: &Arc) -> Result, Error> { +/// Returns the packages from `idents` that have a matching DefinitelyTyped +/// package, along with the name of said package. +/// +/// The Algolia index is only used to make `yarn add` more convenient, so being +/// unable to reach it must never turn into a hard failure: we warn the user and +/// let the install proceed without the `@types` packages. +pub async fn query_algolia(idents: &[Ident], config: &Configuration, http_client: &Arc) -> HashMap { + if idents.is_empty() { + return HashMap::new(); + } + + match query_algolia_impl(idents, http_client).await { + Ok(type_idents) => type_idents, + + Err(err) => { + let warnings = [ + format!("Couldn't query Algolia's npm-search index to detect which packages need a matching @types package ({}); they will be added without it.", err), + "You can disable this lookup by setting enableAutoTypes to false in your .yarnrc.yml (or by setting the YARN_ENABLE_AUTO_TYPES=0 environment variable).".to_string(), + ]; + + // The lookup happens before `yarn add` opens the install report, so + // we usually have to open a report of our own to be heard. + if !emit_warnings(&warnings).await { + let report + = StreamReport::new(StreamReportConfig::from_config(config)); + + with_report(report, emit_warnings(&warnings)).await; + } + + HashMap::new() + }, + } +} + +/// Sends the given warnings to the active report, if any. Returns whether a +/// report was active. +async fn emit_warnings(warnings: &[String]) -> bool { + if_active_async(|report| { + for warning in warnings { + report.warn(warning.clone()); + } + }).await +} + +async fn query_algolia_impl(idents: &[Ident], http_client: &Arc) -> Result, Error> { let input_payload = AlgoliaInputPayload { requests: idents.iter().map(|ident| AlgoliaRequest { index_name: "npm-search".to_string(), @@ -57,6 +111,7 @@ pub async fn query_algolia(idents: &[Ident], http_client: &Arc) -> R .body(JsonDocument::to_string(&input_payload).unwrap()) .header("x-algolia-application-id", Some("OFCNCOG2CU")) .header("x-algolia-api-key", Some("e8e1bd300d860104bb8c58453ffa1eb4")) + .timeout(ALGOLIA_TIMEOUT) .send() .await?; diff --git a/packages/zpm/src/commands/add.rs b/packages/zpm/src/commands/add.rs index becd398e..99d066ee 100644 --- a/packages/zpm/src/commands/add.rs +++ b/packages/zpm/src/commands/add.rs @@ -113,7 +113,7 @@ async fn expand_with_types<'a>(install_context: &InstallContext<'a>, _resolve_op } let type_idents - = query_algolia(&search_space, &project.http_client).await?; + = query_algolia(&search_space, &project.config, &project.http_client).await; for (ident, _) in type_idents { let Some((descriptor, request)) = candidate_requests.remove(&ident) else { diff --git a/packages/zpm/src/commands/debug/check_requirements.rs b/packages/zpm/src/commands/debug/check_requirements.rs index 496dd10a..e1f56a9c 100644 --- a/packages/zpm/src/commands/debug/check_requirements.rs +++ b/packages/zpm/src/commands/debug/check_requirements.rs @@ -21,7 +21,7 @@ impl CheckRequirements { = Project::new(None).await?; let systems - = project.config.settings.supported_architectures.to_systems(); + = project.config.settings.supported_systems(); println!("Systems: {:#?}", systems); println!(); diff --git a/packages/zpm/src/commands/info.rs b/packages/zpm/src/commands/info.rs index 32210db0..e50b6efa 100644 --- a/packages/zpm/src/commands/info.rs +++ b/packages/zpm/src/commands/info.rs @@ -241,8 +241,10 @@ impl Info { .filter(|descriptor| !resolution.peer_dependencies.contains_key(&descriptor.ident)) .map(|descriptor| (descriptor, &install_state.resolution_tree.descriptor_to_locator[descriptor])) .map(|(descriptor, locator)| { + // The descriptors are always reported as seen by the base package, but + // the locators keep their virtual instances when `--virtuals` is set. if self.virtuals { - DescriptorResolution::new(descriptor.clone(), locator.clone()) + DescriptorResolution::new(descriptor.physical_descriptor(), locator.clone()) } else { DescriptorResolution::new(descriptor.physical_descriptor(), locator.physical_locator()) } diff --git a/packages/zpm/src/commands/npm/audit.rs b/packages/zpm/src/commands/npm/audit.rs index 18fd724f..0b420821 100644 --- a/packages/zpm/src/commands/npm/audit.rs +++ b/packages/zpm/src/commands/npm/audit.rs @@ -421,7 +421,20 @@ impl Audit { if self.recursive && first_visit { if let Some(resolution) = install_state.resolution_tree.locator_resolutions.get(&locator) { - for descriptor in resolution.dependencies.values() { + let workspace + = project.workspaces.iter() + .find(|workspace| workspace.locator() == physical_locator); + + for (ident, descriptor) in &resolution.dependencies { + // Workspaces store their development dependencies alongside their regular ones, + // even though those aren't part of the transitive production dependency graph. + let is_dev_dependency + = workspace.is_some_and(|workspace| workspace.manifest.dev_dependencies.contains_key(ident)); + + if is_dev_dependency && !include_dev_dependencies { + continue; + } + if let Some(dep_locator) = install_state.resolution_tree.descriptor_to_locator.get(descriptor) { queue.push((locator.clone(), dep_locator.clone())); } diff --git a/packages/zpm/src/http.rs b/packages/zpm/src/http.rs index bbbba1ff..0a83e6c9 100644 --- a/packages/zpm/src/http.rs +++ b/packages/zpm/src/http.rs @@ -21,6 +21,7 @@ static WARNED_HOSTNAMES: LazyLock>> = LazyLoc pub struct HttpConfig { pub enforce_unsafe_http: bool, pub http_retry: usize, + pub http_timeout: u64, pub unsafe_http_whitelist: Vec>, pub slow_network_timeout: u64, @@ -167,6 +168,19 @@ impl<'a> HttpRequest<'a> { self } + /// Overrides the client-wide timeout for this specific request. It covers + /// the whole exchange (connection included), which makes it suitable to + /// bound requests that must not stall the command they're part of. Note + /// that the request timeout is never allowed to exceed the global + /// `httpTimeout` setting. + pub fn timeout(mut self, timeout: Duration) -> Self { + let bounded_timeout + = std::cmp::min(timeout, Duration::from_millis(self.client.config.http_timeout)); + + self.builder = self.builder.timeout(bounded_timeout); + self + } + pub async fn send(self) -> Result { let mut retry_count = 0; @@ -428,6 +442,7 @@ impl HttpClient { let config = HttpConfig { enforce_unsafe_http: config.settings.enforce_unsafe_http.value, http_retry: config.settings.http_retry.value, + http_timeout: config.settings.http_timeout.value, unsafe_http_whitelist: config.settings.unsafe_http_whitelist.clone(), slow_network_timeout: config.settings.slow_network_timeout.value, diff --git a/packages/zpm/src/http_npm.rs b/packages/zpm/src/http_npm.rs index 97dcb482..cf461a79 100644 --- a/packages/zpm/src/http_npm.rs +++ b/packages/zpm/src/http_npm.rs @@ -1020,6 +1020,14 @@ async fn ask_for_otp(params: &NpmHttpParams<'_>, response: &Response) -> Result< render_otp_notice(&response).await; + // Nobody's there to answer the prompt when we're not attached to a + // terminal; erroring out is better than hanging forever. + if !zpm_utils::is_terminal() { + return Err(Error::AuthenticationError( + "The registry requires additional authentication, but Yarn isn't running in an interactive terminal; rerun this command with --otp ".to_string() + )); + } + let report_guard = current_report().await; diff --git a/packages/zpm/src/install.rs b/packages/zpm/src/install.rs index f3d1284e..08057b14 100644 --- a/packages/zpm/src/install.rs +++ b/packages/zpm/src/install.rs @@ -8,7 +8,7 @@ use itertools::Itertools; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use zpm_config::PackageExtension; use zpm_primitives::{Descriptor, GitRange, Ident, Locator, PatchRange, PeerRange, Range, Reference, RegistrySemverRange, RegistryTagRange, SemverDescriptor, SemverPeerRange, WorkspaceIdentRange}; -use zpm_utils::{DataType, Hash64, Hash64Writer, IoResultExt, Path, System, ToHumanString, UrlEncoded, scc_tarjan_pearce}; +use zpm_utils::{DataType, Hash64, Hash64Writer, IoResultExt, Path, SystemSet, ToHumanString, UrlEncoded, scc_tarjan_pearce}; use rkyv::Archive; use serde::{Deserialize, Serialize}; use zpm_utils::{FromFileString, ToFileString}; @@ -21,7 +21,7 @@ use crate::{ pub struct InstallContext<'a> { pub package_cache: Option<&'a CompositeCache>, pub project: Option<&'a Project>, - pub systems: Option<&'a Vec>, + pub systems: Option<&'a Vec>, pub check_checksums: bool, pub check_resolutions: bool, pub prune_dev_dependencies: bool, @@ -138,7 +138,7 @@ impl<'a> InstallContext<'a> { self } - pub fn with_systems(mut self, systems: Option<&'a Vec>) -> Self { + pub fn with_systems(mut self, systems: Option<&'a Vec>) -> Self { self.systems = systems; self } @@ -709,7 +709,14 @@ fn verify_resolution_consistency(descriptor: &Descriptor, locator: &Locator) -> return Err(mismatch()); } - if !range_params.range.check(resolved_version) { + // The npm resolver lets a `*` range fall back on prereleases when + // the package doesn't have any stable version, so we must accept + // them here as well (otherwise the very resolutions we produce + // would be reported as inconsistent). + let in_range = range_params.range.check(resolved_version) + || (range_params.range.is_wildcard() && range_params.range.check_ignore_rc(resolved_version)); + + if !in_range { return Err(mismatch()); } }, diff --git a/packages/zpm/src/linker/nm/mod.rs b/packages/zpm/src/linker/nm/mod.rs index 6deac142..7ae16399 100644 --- a/packages/zpm/src/linker/nm/mod.rs +++ b/packages/zpm/src/linker/nm/mod.rs @@ -12,11 +12,29 @@ pub mod hoist; const EXPECT_CHILDREN: &str = "All nodes should be expanded by the end of the hoisting process"; -fn collect_binaries_from_dependencies(install: &Install, children: &BTreeMap, work_tree: &WorkTree) -> BTreeMap { +/// Collects the binaries exposed by the packages sitting in a node's +/// `node_modules` folder. Names may collide (two packages exporting the +/// same bin, often through aliases); the node's own dependencies win +/// over packages that merely got hoisted next to them, and otherwise +/// the first candidate wins - same tie-break as Berry's +/// `createBinSymlinkMap`. +fn collect_binaries_from_dependencies(install: &Install, node: &hoist::WorkNode, work_tree: &WorkTree) -> BTreeMap { let mut binaries = BTreeMap::new(); - for (ident, child_idx) in children { + let children + = node.children.as_ref() + .expect(EXPECT_CHILDREN); + + let is_direct_dependency = |ident: &Ident| { + node.dependencies.contains_key(ident) + }; + + let children_by_priority + = children.iter().filter(|(ident, _)| is_direct_dependency(ident)) + .chain(children.iter().filter(|(ident, _)| !is_direct_dependency(ident))); + + for (ident, child_idx) in children_by_priority { let child_node = &work_tree.nodes[*child_idx]; @@ -26,7 +44,8 @@ fn collect_binaries_from_dependencies(install: &Install, children: &BTreeMap, descriptor: let registry_data: RegistryMetadata = JsonDocument::hydrate_from_slice(&bytes[..])?; - // Iterate in reverse order as we assume that users will most likely use newer versions. - for (version, manifest) in registry_data.versions.iter().rev() { - // Skip if the version is not in the range - if !params.range.check(version) { - continue; - } - + let is_approved = |version: &zpm_semver::Version| { // Skip if the version is more recent than the minimum age gate let time = if !minimal_age_gate.is_zero() { registry_data.time.as_ref().and_then(|map| map.get(version)) @@ -258,10 +252,29 @@ pub async fn resolve_semver_descriptor(context: &InstallContext<'_>, descriptor: None }; - if !is_package_approved(context, package_ident, version, time, minimal_age_gate) { - continue; - } + is_package_approved(context, package_ident, version, time, minimal_age_gate) + }; + + // Iterate in reverse order as we assume that users will most likely use newer versions. + let mut in_range = registry_data.versions.iter().rev() + .filter(|(version, _)| params.range.check(version)) + .peekable(); + + let candidate = match in_range.peek() { + Some(_) => in_range.find(|(version, _)| is_approved(version)), + + // The `*` range never matches a prerelease, so a package whose only + // published versions are prereleases wouldn't be installable at all; + // when nothing matched we thus retry while tolerating them. We keep + // this scoped to `*` so that the semantics of other ranges are left + // untouched. + None if params.range.is_wildcard() => registry_data.versions.iter().rev() + .find(|(version, _)| params.range.check_ignore_rc(*version) && is_approved(version)), + + None => None, + }; + if let Some((version, manifest)) = candidate { let manifest = JsonDocument::hydrate_from_value(manifest)?; diff --git a/tests/acceptance-tests/pkg-tests-core/sources/utils/tests.ts b/tests/acceptance-tests/pkg-tests-core/sources/utils/tests.ts index e9e4d7d2..6505b574 100644 --- a/tests/acceptance-tests/pkg-tests-core/sources/utils/tests.ts +++ b/tests/acceptance-tests/pkg-tests-core/sources/utils/tests.ts @@ -764,7 +764,7 @@ export const startPackageServer = ({type}: {type: keyof typeof packageServerUrls }), )), ), - time, + ...(!name.startsWith(`no-time-`) ? {time} : {}), [`dist-tags`]: { latest: semver.maxSatisfying(versions, `*`), ...distTags, diff --git a/tests/acceptance-tests/pkg-tests-fixtures/packages/no-time-deps-1.0.0/index.js b/tests/acceptance-tests/pkg-tests-fixtures/packages/no-time-deps-1.0.0/index.js new file mode 100644 index 00000000..bb9c6f68 --- /dev/null +++ b/tests/acceptance-tests/pkg-tests-fixtures/packages/no-time-deps-1.0.0/index.js @@ -0,0 +1,7 @@ +module.exports = require(`./package.json`); + +for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) { + for (const dep of Object.keys(module.exports[key] || {})) { + module.exports[key][dep] = require(dep); + } +} diff --git a/tests/acceptance-tests/pkg-tests-fixtures/packages/no-time-deps-1.0.0/package.json b/tests/acceptance-tests/pkg-tests-fixtures/packages/no-time-deps-1.0.0/package.json new file mode 100644 index 00000000..2d847c3b --- /dev/null +++ b/tests/acceptance-tests/pkg-tests-fixtures/packages/no-time-deps-1.0.0/package.json @@ -0,0 +1,4 @@ +{ + "name": "no-time-deps", + "version": "1.0.0" +} diff --git a/tests/acceptance-tests/pkg-tests-fixtures/packages/one-dep-alias-bins-1.0.0/package.json b/tests/acceptance-tests/pkg-tests-fixtures/packages/one-dep-alias-bins-1.0.0/package.json new file mode 100644 index 00000000..a796e5c0 --- /dev/null +++ b/tests/acceptance-tests/pkg-tests-fixtures/packages/one-dep-alias-bins-1.0.0/package.json @@ -0,0 +1,7 @@ +{ + "name": "one-dep-alias-bins", + "version": "1.0.0", + "dependencies": { + "@fixture/old": "npm:has-bin-entries@1.0.0" + } +} diff --git a/tests/acceptance-tests/pkg-tests-fixtures/packages/prerelease-only-1.0.0-rc.1/index.js b/tests/acceptance-tests/pkg-tests-fixtures/packages/prerelease-only-1.0.0-rc.1/index.js new file mode 100644 index 00000000..bb9c6f68 --- /dev/null +++ b/tests/acceptance-tests/pkg-tests-fixtures/packages/prerelease-only-1.0.0-rc.1/index.js @@ -0,0 +1,7 @@ +module.exports = require(`./package.json`); + +for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) { + for (const dep of Object.keys(module.exports[key] || {})) { + module.exports[key][dep] = require(dep); + } +} diff --git a/tests/acceptance-tests/pkg-tests-fixtures/packages/prerelease-only-1.0.0-rc.1/package.json b/tests/acceptance-tests/pkg-tests-fixtures/packages/prerelease-only-1.0.0-rc.1/package.json new file mode 100644 index 00000000..87e03410 --- /dev/null +++ b/tests/acceptance-tests/pkg-tests-fixtures/packages/prerelease-only-1.0.0-rc.1/package.json @@ -0,0 +1,4 @@ +{ + "name": "prerelease-only", + "version": "1.0.0-rc.1" +} diff --git a/tests/acceptance-tests/pkg-tests-fixtures/packages/prerelease-only-1.0.0-rc.2/index.js b/tests/acceptance-tests/pkg-tests-fixtures/packages/prerelease-only-1.0.0-rc.2/index.js new file mode 100644 index 00000000..bb9c6f68 --- /dev/null +++ b/tests/acceptance-tests/pkg-tests-fixtures/packages/prerelease-only-1.0.0-rc.2/index.js @@ -0,0 +1,7 @@ +module.exports = require(`./package.json`); + +for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) { + for (const dep of Object.keys(module.exports[key] || {})) { + module.exports[key][dep] = require(dep); + } +} diff --git a/tests/acceptance-tests/pkg-tests-fixtures/packages/prerelease-only-1.0.0-rc.2/package.json b/tests/acceptance-tests/pkg-tests-fixtures/packages/prerelease-only-1.0.0-rc.2/package.json new file mode 100644 index 00000000..0d6c3722 --- /dev/null +++ b/tests/acceptance-tests/pkg-tests-fixtures/packages/prerelease-only-1.0.0-rc.2/package.json @@ -0,0 +1,4 @@ +{ + "name": "prerelease-only", + "version": "1.0.0-rc.2" +} diff --git a/tests/acceptance-tests/pkg-tests-specs/sources/commands/info.test.ts b/tests/acceptance-tests/pkg-tests-specs/sources/commands/info.test.ts index 21343029..fc3d3c92 100644 --- a/tests/acceptance-tests/pkg-tests-specs/sources/commands/info.test.ts +++ b/tests/acceptance-tests/pkg-tests-specs/sources/commands/info.test.ts @@ -61,6 +61,26 @@ describe(`Commands`, () => { }), ); + test( + `it should report virtual locators for nested virtual dependencies`, + makeTemporaryEnv({ + dependencies: { + [`peer-deps-lvl0`]: `1.0.0`, + }, + }, async ({path, run, source}) => { + await run(`install`); + + const {stdout} = await run(`info`, `peer-deps-lvl1`, `--recursive`, `--virtuals`, `--json`); + const data = stdout.match(/.*\n/g)!.map(line => JSON.parse(line)); + const base = data.find(entry => entry.value === `peer-deps-lvl1@npm:1.0.0`); + + expect(base.children.Dependencies).toEqual([{ + descriptor: `peer-deps-lvl2@npm:1.0.0`, + locator: expect.stringMatching(/^peer-deps-lvl2@virtual:/), + }]); + }), + ); + test( `it shouldn't print info for other workspaces by default`, makeTemporaryEnv({ diff --git a/tests/acceptance-tests/pkg-tests-specs/sources/commands/npm/audit.test.ts b/tests/acceptance-tests/pkg-tests-specs/sources/commands/npm/audit.test.ts index e03e890d..0d216485 100644 --- a/tests/acceptance-tests/pkg-tests-specs/sources/commands/npm/audit.test.ts +++ b/tests/acceptance-tests/pkg-tests-specs/sources/commands/npm/audit.test.ts @@ -82,6 +82,34 @@ describe(`Commands`, () => { }), ); + test( + `it shouldn't audit development dependencies of nested workspaces in production`, + makeTemporaryEnv({ + private: true, + workspaces: [ + `packages/*`, + ], + dependencies: { + [`workspace-dependency`]: `workspace:*`, + }, + }, async ({path, run, source}) => { + const workspacePath = ppath.join(path, `packages/workspace-dependency`); + await xfs.mkdirpPromise(workspacePath); + await xfs.writeJsonPromise(ppath.join(workspacePath, Filename.manifest), { + name: `workspace-dependency`, + version: `1.0.0`, + devDependencies: { + [`vulnerable`]: `1.0.0`, + }, + }); + + await run(`install`); + + await run(`npm`, `audit`, `--recursive`, `--environment=production`); + await expect(run(`npm`, `audit`, `--recursive`, `--json`)).rejects.toThrow(/"https:\/\/example\.com\/advisories\/1"/); + }), + ); + test( `it should also audit only development packages if requested`, makeTemporaryEnv({ diff --git a/tests/acceptance-tests/pkg-tests-specs/sources/commands/publish.test.ts b/tests/acceptance-tests/pkg-tests-specs/sources/commands/publish.test.ts index d8a60513..60a6a216 100644 --- a/tests/acceptance-tests/pkg-tests-specs/sources/commands/publish.test.ts +++ b/tests/acceptance-tests/pkg-tests-specs/sources/commands/publish.test.ts @@ -88,6 +88,22 @@ describe(`publish`, () => { })).resolves.toBeTruthy(); })); + test(`should fail rather than prompt for an otp when not attached to a terminal`, makeTemporaryEnv({ + name: `otp-prompt-required`, + version: `1.0.0`, + }, async ({path, run, source}) => { + await run(`install`); + + await expect(run(`npm`, `publish`, { + env: { + // Otherwise the OTP prompt would be short-circuited before we get a + // chance to detect that we're not running in an interactive terminal + YARN_IS_TEST_ENV: undefined, + YARN_NPM_AUTH_TOKEN: validLogins.otpUser.npmAuthToken, + }, + })).rejects.toThrowError(/isn't running in an interactive terminal/); + })); + test(`should publish a package with the readme content`, makeTemporaryEnv({ name: `readme-required`, version: `1.0.0`, diff --git a/tests/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts b/tests/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts index c31b3be2..74678e46 100644 --- a/tests/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts +++ b/tests/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts @@ -278,6 +278,48 @@ describe(`Features`, () => { }), ); + test( + `it should inherit the global minimum release age in rules that don't set it`, + makeTemporaryEnv({ + dependencies: {[`@scoped/release-date`]: `^1.0.0`}, + }, { + npmMinimalAgeGate: 0, + }, async ({run, source}) => { + await run(`config`, `set`, `packageRules`, `--json`, JSON.stringify([{ + packageFilter: `@scoped/*`, + npmAlwaysAuth: false, + }])); + + await run(`install`); + + await expect(source(`require('@scoped/release-date/package.json')`)).resolves.toMatchObject({ + name: `@scoped/release-date`, + version: `1.1.2`, + }); + }), + ); + + test( + `it should inherit the global minimum release age in source rules that don't set it`, + makeTemporaryEnv({ + dependencies: {[`@scoped/release-date`]: `^1.0.0`}, + }, { + npmMinimalAgeGate: 0, + }, async ({run, source}) => { + await run(`config`, `set`, `sourceRules`, `--json`, JSON.stringify([{ + ecosystemFilter: `npm`, + npmAlwaysAuth: false, + }])); + + await run(`install`); + + await expect(source(`require('@scoped/release-date/package.json')`)).resolves.toMatchObject({ + name: `@scoped/release-date`, + version: `1.1.2`, + }); + }), + ); + test( `it should work with scoped packages`, makeTemporaryEnv({ @@ -411,5 +453,21 @@ describe(`Features`, () => { }), ); }); + + describe(`packages with no release time metadata (e.g. GitHub Packages)`, () => { + test( + `it should install a package with no release time even if npmMinimalAgeGate is set`, + makeTemporaryEnv({}, { + npmMinimalAgeGate: `1d`, + }, async ({run, source}) => { + await run(`add`, `no-time-deps`); + + await expect(source(`require('no-time-deps/package.json')`)).resolves.toMatchObject({ + name: `no-time-deps`, + version: `1.0.0`, + }); + }), + ); + }); }); }); diff --git a/tests/acceptance-tests/pkg-tests-specs/sources/features/prunedNativeDeps.test.ts b/tests/acceptance-tests/pkg-tests-specs/sources/features/prunedNativeDeps.test.ts index e06e4658..198c1c7c 100644 --- a/tests/acceptance-tests/pkg-tests-specs/sources/features/prunedNativeDeps.test.ts +++ b/tests/acceptance-tests/pkg-tests-specs/sources/features/prunedNativeDeps.test.ts @@ -170,6 +170,145 @@ describe(`Features`, () => { }]); })); + it(`should only fetch the exact architectures listed when using the list form`, makeTemporaryEnv({ + dependencies: { + [`optional-native`]: `1.0.0`, + }, + }, async ({path, run}) => { + // The matrix form would fetch the cross product of both entries (and + // thus also fetch native-foo-x86 and native-bar-x64); the list form only + // fetches packages compatible with one of the entries taken as a whole. + await xfs.writeJsonPromise(ppath.join(path, Filename.rc), { + supportedArchitectures: [{ + os: `foo`, + cpu: `x64`, + libc: `glibc`, + }, { + os: `bar`, + cpu: `x86`, + libc: `musl`, + }], + }); + + const recording = await startRegistryRecording(async () => { + await run(`install`); + }); + + const tarballRequests = recording.filter(request => { + return request.type === RequestType.PackageTarball; + }).sort((a, b) => { + const aJson = JSON.stringify(a); + const bJson = JSON.stringify(b); + return aJson < bJson ? -1 : aJson > bJson ? 1 : 0; + }); + + expect(tarballRequests).toEqual([{ + type: RequestType.PackageTarball, + localName: `native-foo-x64`, + version: `1.0.0`, + }, { + type: RequestType.PackageTarball, + localName: `native-libc-glibc`, + version: `1.0.0`, + }, { + type: RequestType.PackageTarball, + localName: `native-libc-musl`, + version: `1.0.0`, + }, { + type: RequestType.PackageTarball, + localName: `optional-native`, + version: `1.0.0`, + }]); + })); + + it(`should support the list form with multiple values and nulls inside a single entry`, makeTemporaryEnv({ + dependencies: { + [`optional-native`]: `1.0.0`, + }, + }, async ({path, run, source}) => { + await xfs.writeJsonPromise(ppath.join(path, Filename.rc), { + supportedArchitectures: [{ + os: `foo`, + cpu: [`x64`, `x86`], + libc: null, + }], + }); + + const recording = await startRegistryRecording(async () => { + await run(`install`); + }); + + const tarballRequests = recording.filter(request => { + return request.type === RequestType.PackageTarball; + }).sort((a, b) => { + const aJson = JSON.stringify(a); + const bJson = JSON.stringify(b); + return aJson < bJson ? -1 : aJson > bJson ? 1 : 0; + }); + + expect(tarballRequests).toEqual([{ + type: RequestType.PackageTarball, + localName: `native-foo-x64`, + version: `1.0.0`, + }, { + type: RequestType.PackageTarball, + localName: `native-foo-x86`, + version: `1.0.0`, + }, { + type: RequestType.PackageTarball, + localName: `native-libc-glibc`, + version: `1.0.0`, + }, { + type: RequestType.PackageTarball, + localName: `native-libc-musl`, + version: `1.0.0`, + }, { + type: RequestType.PackageTarball, + localName: `optional-native`, + version: `1.0.0`, + }]); + })); + + it(`should treat a list with a single entry just like the legacy object form`, makeTemporaryEnv({ + dependencies: { + [`optional-native`]: `1.0.0`, + }, + }, async ({path, run, source}) => { + await xfs.writeJsonPromise(ppath.join(path, Filename.rc), { + supportedArchitectures: [{ + os: [`foo`], + cpu: [`x64`], + libc: [`glibc`], + }], + }); + + const recording = await startRegistryRecording(async () => { + await run(`install`); + }); + + const tarballRequests = recording.filter(request => { + return request.type === RequestType.PackageTarball; + }).sort((a, b) => { + const aJson = JSON.stringify(a); + const bJson = JSON.stringify(b); + return aJson < bJson ? -1 : aJson > bJson ? 1 : 0; + }); + + expect(tarballRequests).toEqual([{ + type: RequestType.PackageTarball, + localName: `native-foo-x64`, + version: `1.0.0`, + }, { + type: RequestType.PackageTarball, + localName: `native-libc-glibc`, + version: `1.0.0`, + }, { + type: RequestType.PackageTarball, + localName: `optional-native`, + version: `1.0.0`, + }]); + })); + it(`should produce a stable lockfile, regardless of the architecture`, makeTemporaryEnv({ dependencies: { [`optional-native`]: `1.0.0`, diff --git a/tests/acceptance-tests/pkg-tests-specs/sources/node-modules.test.ts b/tests/acceptance-tests/pkg-tests-specs/sources/node-modules.test.ts index 001602f3..5b44d2e4 100644 --- a/tests/acceptance-tests/pkg-tests-specs/sources/node-modules.test.ts +++ b/tests/acceptance-tests/pkg-tests-specs/sources/node-modules.test.ts @@ -244,6 +244,34 @@ describe(`Node Modules`, () => { ), ); + test(`should prefer direct dependency bins over transitive dependency bins`, + makeTemporaryEnv( + { + dependencies: { + [`@fixture/native`]: `npm:has-bin-entries@2.0.0`, + [`has-bin-entries`]: `npm:one-dep-alias-bins@1.0.0`, + }, + }, + { + nodeLinker: `node-modules`, + }, + async ({path, run}) => { + await run(`install`); + + // The direct dependency must win over the transitive alias, even + // though the transitive one sorts first + const binSymlink = await xfs.readlinkPromise(npath.toPortablePath(`${path}/node_modules/.bin/has-bin-entries-with-relative-require`)); + expect(binSymlink).toContain(`@fixture/native`); + + if (process.platform !== `win32`) { + await expect(run(`node`, `${path}/node_modules/.bin/has-bin-entries-with-relative-require`)).resolves.toMatchObject({ + stdout: `2.0.0\n`, + }); + } + }, + ), + ); + test(`should support dependency via link: protocol to a missing folder`, makeTemporaryEnv( { diff --git a/tests/acceptance-tests/pkg-tests-specs/sources/plugins/plugin-typescript.test.ts b/tests/acceptance-tests/pkg-tests-specs/sources/plugins/plugin-typescript.test.ts index ac27bc49..143d4209 100644 --- a/tests/acceptance-tests/pkg-tests-specs/sources/plugins/plugin-typescript.test.ts +++ b/tests/acceptance-tests/pkg-tests-specs/sources/plugins/plugin-typescript.test.ts @@ -44,6 +44,37 @@ describe(`Plugins`, () => { }), ); + test( + `it should warn and add the package without @types when the Algolia index can't be reached`, + makeTemporaryEnv({}, { + tsEnableAutoTypes: true, + }, async ({path, run, source}) => { + // Simulates a network where the Algolia index can't be reached (for + // instance a corporate proxy silently dropping the request); the + // registry is configured through the environment and stays reachable + await xfs.writeFilePromise(ppath.join(path, `.yarnrc.yml`), [ + `networkSettings:`, + ` "*.algolia.net":`, + ` enableNetwork: false`, + ``, + ].join(`\n`)); + + const {stdout} = await run(`add`, `is-number`); + + expect(stdout).toMatch(/Couldn't query Algolia's npm-search index/); + + const manifest = await readManifest(path); + + expect(manifest).toMatchObject({ + dependencies: { + [`is-number`]: `^2.0.0`, + }, + }); + + expect(manifest).not.toHaveProperty(`devDependencies`); + }), + ); + test( `it should automatically enable automatic @types insertion when a tsconfig is detected in the current workspace`, makeTemporaryMonorepoEnv({ diff --git a/tests/acceptance-tests/pkg-tests-specs/sources/protocols/npm.test.js b/tests/acceptance-tests/pkg-tests-specs/sources/protocols/npm.test.js index ac21cba5..47bc3ffa 100644 --- a/tests/acceptance-tests/pkg-tests-specs/sources/protocols/npm.test.js +++ b/tests/acceptance-tests/pkg-tests-specs/sources/protocols/npm.test.js @@ -164,5 +164,52 @@ describe(`Protocols`, () => { }, ), ); + + test( + `it should resolve a "*" range to a prerelease when the package only has prereleases`, + makeTemporaryEnv( + { + dependencies: {[`prerelease-only`]: `*`}, + }, + async ({run, source}) => { + await run(`install`); + + await expect(source(`require('prerelease-only')`)).resolves.toMatchObject({ + name: `prerelease-only`, + version: `1.0.0-rc.2`, + }); + }, + ), + ); + + test( + `it should resolve a "*" range to the stable version when the package has one`, + makeTemporaryEnv( + { + dependencies: {[`no-deps-tags`]: `*`}, + }, + async ({run, source}) => { + await run(`install`); + + await expect(source(`require('no-deps-tags')`)).resolves.toMatchObject({ + name: `no-deps-tags`, + version: `1.0.0`, + }); + }, + ), + ); + + test( + `it should pass --check-resolutions for a "*" range that only has prereleases`, + makeTemporaryEnv( + { + dependencies: {[`prerelease-only`]: `*`}, + }, + async ({run}) => { + await run(`install`); + await run(`install`, `--check-resolutions`); + }, + ), + ); }); }); diff --git a/yarn.lock b/yarn.lock index 1a099fce..a326986b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,14 +3,14 @@ "version": 9 }, "workspaces": { - "@yarnpkg/monorepo": "eddbbccdb321f78248af868f80ac6c9a9a2c7ac3703e8d1e9a52a14f8f61231eadae404f19111b09d93b5d999e71bfe46b009500ee55183f4bfcd935c9db7638", + "@yarnpkg/monorepo": "e537b0a7fa1ecc3f6ec4a5fd8fdaae1e45122965e971733a9766ed95ba4d59148b143e36302160003d14318320f997ad62ca3c603c516ff7ad80567083ed1275", "@yarnpkg/website": "26cb7cb1ffe53aa62a7055a447eae22dc1c5dd3e96e1222066c65209b4058255598efb57d882e759bf615eac76d19337a6f60665f0205bf7dc101368e48abc77", "@yarnpkg/zpm-constraints": "a602e3cc3ea1dd931ef50091753a9f1de3a06ff9ee0efb29c8222fb3491132ccbac1581220770e74dcf06e2c30189f636b1d7eb691bfdeff36012d3169630bc6", - "@yarnpkg/zpm-daemon-ui": "c7ca05eb5a4d7c70d789b59619d2b12edf79de647fcd3495cec8d50914a2cd74b51dad090c562716eab187f7d407122678fd2ee2aafeb858e55a81599c6ce530", - "acceptance-tests": "2c1ffd88b6971fe22e14338775531c02f85ab6101125903525acebdfdfc9d86df2763ab2160473a70f7c0ccb9be9a8f7a129130f8c4ccca9dd8d0745e05c12ab", + "@yarnpkg/zpm-daemon-ui": "4eb540829382d117e5b625f696d92a376384c8df02d519c359dc734322d75bae6f387358643459dc7b83a113af713e52bca493804ec85c30ca8d2ac9ad314f2c", + "acceptance-tests": "f743c449b709e94cbbea6f60c2281c9d24a30c2ce7cdfa52af51c393ef52b93b4363b9962d64cf976f326b8ecd715f23352766d1a45b71be188e4972f0218bb8", "pkg-tests-core": "27dc1794f148dca7d11639e6ea0877781dcf88daf2e38af45bae8a0f9f259d4f49cb3f960e27849a3ee1e9d702da717d0060409c37fe6b26b23e509382d945e9", "pkg-tests-fixtures": "634e2d39424349e30ef9ace3cbe374d6c2b404ab29491084908f538e4f9823567caa6d900e0c15e78c1515ed8b1418294c0e3063027fb7475982d0ddd62c2b64", - "pkg-tests-specs": "377aaa5abdb3d7c57990e82ed0fa6e4f0ac62d7fbcdd45dd3e69ab4eb1406232d7181d99bf55cb6aace05c7f94eccbe198b834dfcc25dd400e3084df68a22195" + "pkg-tests-specs": "46fd5503c56d268ea878a6655f569016155248430d7362464ea0d44edef3c998304e9cf67c9a95dd9358c3279b2a8fc71460f61586b73fa4dc0a8e7d76b9421d" }, "entries": { "@algolia/abtesting@npm:1.18.1": {