1pub mod dev_engines;
2pub mod workspace;
3
4pub use dev_engines::{DevEngineDependency, DevEngines, OnFail, dev_engines_tolerant};
5pub use workspace::{JailBuildPermission, WorkspaceConfig};
6
7use serde::{Deserialize, Deserializer, Serialize};
8use std::collections::{BTreeMap, BTreeSet};
9use std::path::{Path, PathBuf};
10
11pub fn engines_tolerant<'de, D>(de: D) -> Result<BTreeMap<String, String>, D::Error>
28where
29 D: Deserializer<'de>,
30{
31 let value: Option<serde_json::Value> = Option::deserialize(de)?;
32 Ok(match value {
33 None
34 | Some(serde_json::Value::Null)
35 | Some(serde_json::Value::Array(_))
36 | Some(serde_json::Value::String(_)) => BTreeMap::new(),
37 Some(serde_json::Value::Object(m)) => m
38 .into_iter()
39 .filter_map(|(k, v)| match v {
40 serde_json::Value::String(s) => Some((k, s)),
41 _ => None,
42 })
43 .collect(),
44 Some(other) => {
45 return Err(serde::de::Error::custom(format!(
48 "engines: expected a map, got {}",
49 match other {
50 serde_json::Value::Number(_) => "number",
51 serde_json::Value::Bool(_) => "boolean",
52 _ => unreachable!("engines: unexpected value variant"),
53 }
54 )));
55 }
56 })
57}
58
59pub fn scripts_tolerant<'de, D>(de: D) -> Result<BTreeMap<String, String>, D::Error>
67where
68 D: Deserializer<'de>,
69{
70 let value: Option<serde_json::Value> = Option::deserialize(de)?;
71 Ok(match value {
72 None | Some(serde_json::Value::Null) => BTreeMap::new(),
73 Some(serde_json::Value::Object(m)) => m
74 .into_iter()
75 .filter_map(|(k, v)| match v {
76 serde_json::Value::String(s) => Some((k, s)),
77 _ => None,
78 })
79 .collect(),
80 Some(_) => BTreeMap::new(),
81 })
82}
83
84pub fn deps_tolerant<'de, D>(de: D) -> Result<BTreeMap<String, String>, D::Error>
93where
94 D: Deserializer<'de>,
95{
96 let value: Option<serde_json::Value> = Option::deserialize(de)?;
97 Ok(match value {
98 None | Some(serde_json::Value::Null) => BTreeMap::new(),
99 Some(serde_json::Value::Object(m)) => m
100 .into_iter()
101 .filter_map(|(k, v)| match v {
102 serde_json::Value::String(s) => Some((k, s)),
103 _ => None,
104 })
105 .collect(),
106 Some(_) => BTreeMap::new(),
107 })
108}
109
110#[derive(Debug, Clone, Default, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct UpdateConfig {
113 #[serde(default, skip_serializing_if = "Vec::is_empty")]
114 pub ignore_dependencies: Vec<String>,
115}
116
117#[derive(Debug, Clone, Default, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase", from = "PackageJsonRaw")]
126pub struct PackageJson {
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub name: Option<String>,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub version: Option<String>,
131 #[serde(
132 default,
133 deserialize_with = "deps_tolerant",
134 skip_serializing_if = "BTreeMap::is_empty"
135 )]
136 pub dependencies: BTreeMap<String, String>,
137 #[serde(
138 default,
139 deserialize_with = "deps_tolerant",
140 skip_serializing_if = "BTreeMap::is_empty"
141 )]
142 pub dev_dependencies: BTreeMap<String, String>,
143 #[serde(
144 default,
145 deserialize_with = "deps_tolerant",
146 skip_serializing_if = "BTreeMap::is_empty"
147 )]
148 pub peer_dependencies: BTreeMap<String, String>,
149 #[serde(
150 default,
151 deserialize_with = "deps_tolerant",
152 skip_serializing_if = "BTreeMap::is_empty"
153 )]
154 pub optional_dependencies: BTreeMap<String, String>,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub update_config: Option<UpdateConfig>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub bundled_dependencies: Option<BundledDependencies>,
166 #[serde(
167 default,
168 deserialize_with = "scripts_tolerant",
169 skip_serializing_if = "BTreeMap::is_empty"
170 )]
171 pub scripts: BTreeMap<String, String>,
172 #[serde(
178 default,
179 deserialize_with = "engines_tolerant",
180 skip_serializing_if = "BTreeMap::is_empty"
181 )]
182 pub engines: BTreeMap<String, String>,
183 #[serde(
188 default,
189 deserialize_with = "dev_engines_tolerant",
190 skip_serializing_if = "Option::is_none"
191 )]
192 pub dev_engines: Option<DevEngines>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub workspaces: Option<Workspaces>,
195 #[serde(flatten)]
196 pub extra: BTreeMap<String, serde_json::Value>,
197}
198
199#[derive(Debug, Default, Deserialize)]
215#[serde(rename_all = "camelCase")]
216struct PackageJsonRaw {
217 name: Option<String>,
218 version: Option<String>,
219 #[serde(default, deserialize_with = "deps_tolerant")]
220 dependencies: BTreeMap<String, String>,
221 #[serde(default, deserialize_with = "deps_tolerant")]
222 dev_dependencies: BTreeMap<String, String>,
223 #[serde(default, deserialize_with = "deps_tolerant")]
224 peer_dependencies: BTreeMap<String, String>,
225 #[serde(default, deserialize_with = "deps_tolerant")]
226 optional_dependencies: BTreeMap<String, String>,
227 #[serde(default)]
228 update_config: Option<UpdateConfig>,
229 #[serde(default, rename = "bundledDependencies")]
230 bundled_dependencies: Option<BundledDependencies>,
231 #[serde(default, rename = "bundleDependencies")]
232 bundle_dependencies_alias: Option<BundledDependencies>,
233 #[serde(default, deserialize_with = "scripts_tolerant")]
234 scripts: BTreeMap<String, String>,
235 #[serde(default, deserialize_with = "engines_tolerant")]
236 engines: BTreeMap<String, String>,
237 #[serde(default, deserialize_with = "dev_engines_tolerant")]
238 dev_engines: Option<DevEngines>,
239 #[serde(default)]
240 workspaces: Option<Workspaces>,
241 #[serde(flatten)]
242 extra: BTreeMap<String, serde_json::Value>,
243}
244
245impl From<PackageJsonRaw> for PackageJson {
246 fn from(raw: PackageJsonRaw) -> Self {
247 Self {
248 name: raw.name,
249 version: raw.version,
250 dependencies: raw.dependencies,
251 dev_dependencies: raw.dev_dependencies,
252 peer_dependencies: raw.peer_dependencies,
253 optional_dependencies: raw.optional_dependencies,
254 update_config: raw.update_config,
255 bundled_dependencies: raw.bundled_dependencies.or(raw.bundle_dependencies_alias),
256 scripts: raw.scripts,
257 engines: raw.engines,
258 dev_engines: raw.dev_engines,
259 workspaces: raw.workspaces,
260 extra: raw.extra,
261 }
262 }
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
270#[serde(untagged)]
271pub enum BundledDependencies {
272 List(Vec<String>),
273 All(bool),
274}
275
276impl BundledDependencies {
277 pub fn names<'a>(&'a self, dependencies: &'a BTreeMap<String, String>) -> Vec<&'a str> {
281 match self {
282 BundledDependencies::List(v) => v.iter().map(String::as_str).collect(),
283 BundledDependencies::All(true) => dependencies.keys().map(String::as_str).collect(),
284 BundledDependencies::All(false) => Vec::new(),
285 }
286 }
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
290#[serde(untagged)]
291pub enum Workspaces {
292 String(String),
298 Array(Vec<String>),
299 Object {
300 packages: Vec<String>,
306 #[serde(default)]
307 nohoist: Vec<String>,
308 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
313 catalog: BTreeMap<String, String>,
314 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
316 catalogs: BTreeMap<String, BTreeMap<String, String>>,
317 },
318}
319
320impl Workspaces {
321 pub fn patterns(&self) -> &[String] {
322 match self {
323 Workspaces::String(s) => std::slice::from_ref(s),
324 Workspaces::Array(v) => v,
325 Workspaces::Object { packages, .. } => packages,
326 }
327 }
328
329 pub fn catalog(&self) -> &BTreeMap<String, String> {
332 static EMPTY: std::sync::OnceLock<BTreeMap<String, String>> = std::sync::OnceLock::new();
333 match self {
334 Workspaces::String(_) | Workspaces::Array(_) => EMPTY.get_or_init(BTreeMap::new),
335 Workspaces::Object { catalog, .. } => catalog,
336 }
337 }
338
339 pub fn catalogs(&self) -> &BTreeMap<String, BTreeMap<String, String>> {
341 static EMPTY: std::sync::OnceLock<BTreeMap<String, BTreeMap<String, String>>> =
342 std::sync::OnceLock::new();
343 match self {
344 Workspaces::String(_) | Workspaces::Array(_) => EMPTY.get_or_init(BTreeMap::new),
345 Workspaces::Object { catalogs, .. } => catalogs,
346 }
347 }
348}
349
350static PACKAGE_JSON_CACHE: aube_util::cache::ProcessCache<PathBuf, PackageJson> =
355 aube_util::cache::ProcessCache::new();
356
357impl PackageJson {
358 pub fn from_path(path: &Path) -> Result<Self, Error> {
359 let _diag = aube_util::diag::Span::new(aube_util::diag::Category::Manifest, "from_path")
360 .with_meta_fn(|| {
361 let display = path
365 .file_name()
366 .map(|n| n.to_string_lossy().into_owned())
367 .unwrap_or_else(|| "package.json".to_string());
368 format!(r#"{{"path":{}}}"#, aube_util::diag::jstr(&display))
369 });
370 let content =
371 std::fs::read_to_string(path).map_err(|e| Error::Io(path.to_path_buf(), e))?;
372 Self::parse(path, content)
373 }
374
375 pub fn from_path_cached(path: &Path) -> Result<std::sync::Arc<Self>, Error> {
379 let key = path.to_path_buf();
380 if let Some(hit) = PACKAGE_JSON_CACHE.get(&key) {
381 return Ok(hit);
382 }
383 let parsed = Self::from_path(path)?;
384 Ok(PACKAGE_JSON_CACHE.get_or_compute(key, || parsed))
385 }
386
387 pub fn parse(path: &Path, content: String) -> Result<Self, Error> {
391 parse_json(path, content)
392 }
393
394 fn pnpm_aube_objects(
403 &self,
404 ) -> impl Iterator<Item = &serde_json::Map<String, serde_json::Value>> {
405 let id = aube_util::embedder();
410 let self_ns = (!id.manifest_namespace.is_empty()).then_some(id.manifest_namespace);
411 id.compatible_names
412 .iter()
413 .copied()
414 .chain(self_ns)
415 .filter_map(|k| self.extra.get(k).and_then(|v| v.as_object()))
416 }
417
418 pub fn pnpm_allow_builds(&self) -> BTreeMap<String, AllowBuildRaw> {
428 let mut out = BTreeMap::new();
429 for ns in self.pnpm_aube_objects() {
430 if let Some(map) = ns.get("allowBuilds").and_then(|v| v.as_object()) {
431 for (k, v) in map {
432 out.insert(k.clone(), AllowBuildRaw::from_json(v));
433 }
434 }
435 }
436 out
437 }
438
439 pub fn pnpm_only_built_dependencies(&self) -> Vec<String> {
448 let mut out = Vec::new();
449 for ns in self.pnpm_aube_objects() {
450 if let Some(arr) = ns.get("onlyBuiltDependencies").and_then(|v| v.as_array()) {
451 push_unique_strs(&mut out, arr);
452 }
453 }
454 out
455 }
456
457 pub fn pnpm_never_built_dependencies(&self) -> Vec<String> {
464 let mut out = Vec::new();
465 for ns in self.pnpm_aube_objects() {
466 if let Some(arr) = ns.get("neverBuiltDependencies").and_then(|v| v.as_array()) {
467 push_unique_strs(&mut out, arr);
468 }
469 }
470 out
471 }
472
473 pub fn trusted_dependencies(&self) -> Vec<String> {
480 let mut out = Vec::new();
481 if let Some(arr) = self
482 .extra
483 .get("trustedDependencies")
484 .and_then(|v| v.as_array())
485 {
486 push_unique_strs(&mut out, arr);
487 }
488 out
489 }
490
491 pub fn npm_package_env(&self) -> Vec<(String, String)> {
510 let mut out = Vec::new();
511 if let Some(name) = &self.name {
512 out.push(("npm_package_name".to_string(), name.clone()));
513 }
514 if let Some(version) = &self.version {
515 out.push(("npm_package_version".to_string(), version.clone()));
516 }
517 for (key, value) in &self.engines {
518 out.push((
519 format!("npm_package_engines_{}", envify_env_key(key)),
520 value.clone(),
521 ));
522 }
523 if let Some(config) = self.extra.get("config") {
524 flatten_json_env("npm_package_config", config, &mut out);
525 }
526 if let Some(bin) = self.extra.get("bin") {
527 flatten_json_env("npm_package_bin", bin, &mut out);
532 }
533 out
534 }
535
536 pub fn pnpm_catalog(&self) -> BTreeMap<String, String> {
543 let mut out = BTreeMap::new();
544 for ns in self.pnpm_aube_objects() {
545 if let Some(map) = ns.get("catalog").and_then(|v| v.as_object()) {
546 for (k, v) in map {
547 if let Some(s) = v.as_str() {
548 out.insert(k.clone(), s.to_string());
549 }
550 }
551 }
552 }
553 out
554 }
555
556 pub fn pnpm_catalogs(&self) -> BTreeMap<String, BTreeMap<String, String>> {
564 let mut out: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
565 for ns in self.pnpm_aube_objects() {
566 if let Some(outer) = ns.get("catalogs").and_then(|v| v.as_object()) {
567 for (name, inner) in outer {
568 let Some(inner) = inner.as_object() else {
569 continue;
570 };
571 let catalog = out.entry(name.clone()).or_default();
572 for (k, v) in inner {
573 if let Some(s) = v.as_str() {
574 catalog.insert(k.clone(), s.to_string());
575 }
576 }
577 }
578 }
579 }
580 out
581 }
582
583 pub fn pnpm_ignored_optional_dependencies(&self) -> BTreeSet<String> {
591 let mut out = BTreeSet::new();
592 for ns in self.pnpm_aube_objects() {
593 if let Some(arr) = ns
594 .get("ignoredOptionalDependencies")
595 .and_then(|v| v.as_array())
596 {
597 out.extend(arr.iter().filter_map(|v| v.as_str().map(String::from)));
598 }
599 }
600 out
601 }
602
603 pub fn bun_patched_dependencies(&self) -> BTreeMap<String, String> {
607 let mut out = BTreeMap::new();
608 if let Some(map) = self
609 .extra
610 .get("patchedDependencies")
611 .and_then(|v| v.as_object())
612 {
613 for (k, v) in map {
614 if let Some(s) = v.as_str() {
615 out.insert(k.clone(), s.to_string());
616 }
617 }
618 }
619 out
620 }
621
622 pub fn pnpm_patched_dependencies(&self) -> BTreeMap<String, String> {
627 let mut out = BTreeMap::new();
628 for ns in self.pnpm_aube_objects() {
629 if let Some(map) = ns.get("patchedDependencies").and_then(|v| v.as_object()) {
630 for (k, v) in map {
631 if let Some(s) = v.as_str() {
632 out.insert(k.clone(), s.to_string());
633 }
634 }
635 }
636 }
637 out
638 }
639
640 pub fn dependencies_meta_injected(&self) -> BTreeSet<String> {
650 let Some(meta) = self
651 .extra
652 .get("dependenciesMeta")
653 .and_then(|v| v.as_object())
654 else {
655 return BTreeSet::new();
656 };
657 meta.iter()
658 .filter_map(|(k, v)| {
659 let injected = v.get("injected").and_then(|b| b.as_bool()).unwrap_or(false);
660 injected.then(|| k.clone())
661 })
662 .collect()
663 }
664
665 pub fn pnpm_supported_architectures(&self) -> (Vec<String>, Vec<String>, Vec<String>) {
673 let mut os = Vec::new();
674 let mut cpu = Vec::new();
675 let mut libc = Vec::new();
676 for ns in self.pnpm_aube_objects() {
677 let Some(sa) = ns.get("supportedArchitectures").and_then(|v| v.as_object()) else {
678 continue;
679 };
680 if let Some(arr) = sa.get("os").and_then(|v| v.as_array()) {
681 push_unique_strs(&mut os, arr);
682 }
683 if let Some(arr) = sa.get("cpu").and_then(|v| v.as_array()) {
684 push_unique_strs(&mut cpu, arr);
685 }
686 if let Some(arr) = sa.get("libc").and_then(|v| v.as_array()) {
687 push_unique_strs(&mut libc, arr);
688 }
689 }
690 (os, cpu, libc)
691 }
692
693 pub fn overrides_map(&self) -> BTreeMap<String, String> {
705 let mut out: BTreeMap<String, String> = BTreeMap::new();
706 let insert = |out: &mut BTreeMap<String, String>,
707 obj: &serde_json::Map<String, serde_json::Value>| {
708 for (k, v) in obj {
709 if let Some(s) = v.as_str()
710 && is_valid_selector_key(k)
711 {
712 out.insert(k.clone(), s.to_string());
713 }
714 }
715 };
716
717 if let Some(obj) = self.extra.get("resolutions").and_then(|v| v.as_object()) {
719 insert(&mut out, obj);
720 }
721
722 for ns in self.pnpm_aube_objects() {
724 if let Some(obj) = ns.get("overrides").and_then(|v| v.as_object()) {
725 insert(&mut out, obj);
726 }
727 }
728
729 if let Some(obj) = self.extra.get("overrides").and_then(|v| v.as_object()) {
731 insert(&mut out, obj);
732 }
733
734 out
735 }
736
737 pub fn direct_dependency_range(&self, name: &str) -> Option<&str> {
744 self.dependencies
745 .get(name)
746 .or_else(|| self.dev_dependencies.get(name))
747 .or_else(|| self.optional_dependencies.get(name))
748 .map(String::as_str)
749 }
750
751 pub fn resolve_override_refs(&self, overrides: &mut BTreeMap<String, String>) -> Vec<String> {
759 let mut unresolved = Vec::new();
760 overrides.retain(|key, value| {
761 let Some(name) = value.strip_prefix('$') else {
762 return true;
763 };
764 match self.direct_dependency_range(name) {
765 Some(range) => {
766 *value = range.to_owned();
767 true
768 }
769 None => {
770 unresolved.push(key.clone());
771 false
772 }
773 }
774 });
775 unresolved
776 }
777
778 pub fn package_extensions(&self) -> BTreeMap<String, serde_json::Value> {
784 let mut out = BTreeMap::new();
785 for ns in self.pnpm_aube_objects() {
786 if let Some(obj) = ns.get("packageExtensions").and_then(|v| v.as_object()) {
787 for (k, v) in obj {
788 out.insert(k.clone(), v.clone());
789 }
790 }
791 }
792 if let Some(obj) = self
793 .extra
794 .get("packageExtensions")
795 .and_then(|v| v.as_object())
796 {
797 for (k, v) in obj {
798 out.insert(k.clone(), v.clone());
799 }
800 }
801 out
802 }
803
804 pub fn allowed_deprecated_versions(&self) -> BTreeMap<String, String> {
809 let mut out = BTreeMap::new();
810 let insert = |out: &mut BTreeMap<String, String>,
811 obj: &serde_json::Map<String, serde_json::Value>| {
812 for (k, v) in obj {
813 if let Some(s) = v.as_str() {
814 out.insert(k.clone(), s.to_string());
815 }
816 }
817 };
818 for ns in self.pnpm_aube_objects() {
819 if let Some(obj) = ns
820 .get("allowedDeprecatedVersions")
821 .and_then(|v| v.as_object())
822 {
823 insert(&mut out, obj);
824 }
825 }
826 if let Some(obj) = self
827 .extra
828 .get("allowedDeprecatedVersions")
829 .and_then(|v| v.as_object())
830 {
831 insert(&mut out, obj);
832 }
833 out
834 }
835
836 pub fn pnpm_peer_dependency_rules_ignore_missing(&self) -> Vec<String> {
843 self.pnpm_peer_dependency_rules_string_list("ignoreMissing")
844 }
845
846 pub fn pnpm_peer_dependency_rules_allow_any(&self) -> Vec<String> {
850 self.pnpm_peer_dependency_rules_string_list("allowAny")
851 }
852
853 pub fn pnpm_peer_dependency_rules_allowed_versions(&self) -> BTreeMap<String, String> {
863 let mut out = BTreeMap::new();
864 for ns in self.pnpm_aube_objects() {
865 let Some(rules) = ns.get("peerDependencyRules").and_then(|v| v.as_object()) else {
866 continue;
867 };
868 let Some(obj) = rules.get("allowedVersions").and_then(|v| v.as_object()) else {
869 continue;
870 };
871 for (k, v) in obj {
872 if let Some(s) = v.as_str() {
873 out.insert(k.clone(), s.to_string());
874 }
875 }
876 }
877 out
878 }
879
880 fn pnpm_peer_dependency_rules_string_list(&self, field: &str) -> Vec<String> {
881 let mut out = Vec::new();
882 for ns in self.pnpm_aube_objects() {
883 let Some(rules) = ns.get("peerDependencyRules").and_then(|v| v.as_object()) else {
884 continue;
885 };
886 let Some(arr) = rules.get(field).and_then(|v| v.as_array()) else {
887 continue;
888 };
889 push_unique_strs(&mut out, arr);
890 }
891 out
892 }
893
894 pub fn update_ignore_dependencies(&self) -> Vec<String> {
900 let mut out = Vec::new();
901 for ns in self.pnpm_aube_objects() {
902 if let Some(arr) = ns
903 .get("updateConfig")
904 .and_then(|v| v.as_object())
905 .and_then(|u| u.get("ignoreDependencies"))
906 .and_then(|v| v.as_array())
907 {
908 out.extend(arr.iter().filter_map(|v| v.as_str().map(String::from)));
909 }
910 }
911 if let Some(update_config) = &self.update_config {
912 out.extend(update_config.ignore_dependencies.iter().cloned());
913 }
914 out.sort();
915 out.dedup();
916 out
917 }
918
919 pub fn all_dependencies(&self) -> impl Iterator<Item = (&str, &str)> {
920 self.dependencies
921 .iter()
922 .chain(self.dev_dependencies.iter())
923 .map(|(k, v)| (k.as_str(), v.as_str()))
924 }
925
926 pub fn production_dependencies(&self) -> impl Iterator<Item = (&str, &str)> {
927 self.dependencies
928 .iter()
929 .map(|(k, v)| (k.as_str(), v.as_str()))
930 }
931}
932
933#[derive(Debug, Clone, PartialEq, Eq)]
938pub enum AllowBuildRaw {
939 Bool(bool),
940 Other(String),
941}
942
943impl AllowBuildRaw {
944 fn from_json(v: &serde_json::Value) -> Self {
945 match v {
946 serde_json::Value::Bool(b) => Self::Bool(*b),
947 serde_json::Value::String(s) => Self::Other(s.clone()),
954 other => Self::Other(other.to_string()),
955 }
956 }
957}
958
959fn is_valid_selector_key(k: &str) -> bool {
966 !k.is_empty()
967}
968
969fn push_unique_strs(dst: &mut Vec<String>, arr: &[serde_json::Value]) {
975 for v in arr {
976 if let Some(s) = v.as_str()
977 && !dst.iter().any(|existing| existing == s)
978 {
979 dst.push(s.to_string());
980 }
981 }
982}
983
984fn envify_env_key(key: &str) -> String {
988 key.chars()
989 .map(|c| {
990 if c.is_ascii_alphanumeric() || c == '_' {
991 c
992 } else {
993 '_'
994 }
995 })
996 .collect()
997}
998
999fn flatten_json_env(prefix: &str, value: &serde_json::Value, out: &mut Vec<(String, String)>) {
1003 match value {
1004 serde_json::Value::Object(map) => {
1005 for (k, v) in map {
1006 flatten_json_env(&format!("{prefix}_{}", envify_env_key(k)), v, out);
1007 }
1008 }
1009 serde_json::Value::Array(arr) => {
1010 for (i, v) in arr.iter().enumerate() {
1011 flatten_json_env(&format!("{prefix}_{i}"), v, out);
1012 }
1013 }
1014 serde_json::Value::String(s) => out.push((prefix.to_string(), s.clone())),
1015 serde_json::Value::Number(n) => out.push((prefix.to_string(), n.to_string())),
1016 serde_json::Value::Bool(b) => out.push((prefix.to_string(), b.to_string())),
1017 serde_json::Value::Null => {}
1018 }
1019}
1020
1021pub fn effective_supported_architectures(
1030 manifest: &PackageJson,
1031 workspace: &workspace::WorkspaceConfig,
1032) -> (Vec<String>, Vec<String>, Vec<String>) {
1033 let (mut os, mut cpu, mut libc) = manifest.pnpm_supported_architectures();
1034 if let Some(ws) = &workspace.supported_architectures {
1035 let extend_unique = |dst: &mut Vec<String>, src: &[String]| {
1036 for s in src {
1037 if !dst.iter().any(|existing| existing == s) {
1038 dst.push(s.clone());
1039 }
1040 }
1041 };
1042 extend_unique(&mut os, &ws.os);
1043 extend_unique(&mut cpu, &ws.cpu);
1044 extend_unique(&mut libc, &ws.libc);
1045 }
1046 (os, cpu, libc)
1047}
1048
1049pub fn effective_ignored_optional_dependencies(
1055 manifest: &PackageJson,
1056 workspace: &workspace::WorkspaceConfig,
1057) -> BTreeSet<String> {
1058 let mut out = manifest.pnpm_ignored_optional_dependencies();
1059 out.extend(workspace.ignored_optional_dependencies.iter().cloned());
1060 out
1061}
1062
1063#[derive(Debug, thiserror::Error, miette::Diagnostic)]
1064pub enum Error {
1065 #[error("failed to read {0}: {1}")]
1066 Io(std::path::PathBuf, std::io::Error),
1067 #[error(transparent)]
1068 #[diagnostic(transparent)]
1069 Parse(Box<ParseError>),
1070 #[error("failed to parse {0}: {1}")]
1071 #[diagnostic(code(ERR_AUBE_MANIFEST_YAML_PARSE))]
1072 YamlParse(std::path::PathBuf, String),
1073}
1074
1075#[derive(Debug, thiserror::Error)]
1084#[error("failed to parse {path}: {message}")]
1085pub struct ParseError {
1086 pub path: std::path::PathBuf,
1087 pub message: String,
1088 pub src: miette::NamedSource<String>,
1089 pub span: miette::SourceSpan,
1090}
1091
1092impl miette::Diagnostic for ParseError {
1093 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
1094 Some(Box::new(aube_codes::errors::ERR_AUBE_MANIFEST_PARSE))
1095 }
1096
1097 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
1098 Some(&self.src)
1099 }
1100
1101 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
1102 Some(Box::new(std::iter::once(
1103 miette::LabeledSpan::new_with_span(Some(self.message.clone()), self.span),
1104 )))
1105 }
1106}
1107
1108impl ParseError {
1109 pub fn from_json_err(path: &Path, content: String, err: &serde_json::Error) -> Self {
1115 let offset = line_col_to_byte_offset(&content, err.line(), err.column());
1116 let len = if offset >= content.len() { 0 } else { 1 };
1123 Self::new(path, content, err.to_string(), offset, len)
1124 }
1125
1126 pub fn from_yaml_err(path: &Path, content: String, err: &yaml_serde::Error) -> Self {
1133 let (offset, len) = match err.location() {
1134 Some(loc) => {
1135 let idx = loc.index().min(content.len());
1136 let len = if idx >= content.len() { 0 } else { 1 };
1137 (idx, len)
1138 }
1139 None => (0, 0),
1140 };
1141 Self::new(path, content, err.to_string(), offset, len)
1142 }
1143
1144 fn new(path: &Path, content: String, message: String, offset: usize, len: usize) -> Self {
1145 ParseError {
1146 path: path.to_path_buf(),
1147 message,
1148 src: miette::NamedSource::new(path.display().to_string(), content),
1149 span: miette::SourceSpan::new(offset.into(), len),
1150 }
1151 }
1152}
1153
1154pub fn parse_json<T: serde::de::DeserializeOwned>(
1158 path: &Path,
1159 content: String,
1160) -> Result<T, Error> {
1161 let content = if let Some(stripped) = content.strip_prefix('\u{FEFF}') {
1168 stripped.to_owned()
1169 } else {
1170 content
1171 };
1172 if let Ok(v) = sonic_rs::from_slice(content.as_bytes()) {
1173 return Ok(v);
1174 }
1175 match serde_json::from_str(&content) {
1176 Ok(v) => Ok(v),
1177 Err(e) => {
1178 let trimmed = content.trim_start();
1179 if trimmed.starts_with("//") || trimmed.starts_with("/*") {
1180 return Err(Error::parse_msg(
1181 path,
1182 content,
1183 "package.json cannot contain JSON comments. \
1184 Remove any `//` or `/* */` lines. aube does not support JSONC for package.json"
1185 .to_string(),
1186 ));
1187 }
1188 Err(Error::parse(path, content, &e))
1189 }
1190 }
1191}
1192
1193pub fn parse_yaml<T: serde::de::DeserializeOwned>(
1200 path: &Path,
1201 content: String,
1202) -> Result<T, Error> {
1203 match yaml_serde::from_str(&content) {
1204 Ok(v) => Ok(v),
1205 Err(e) => Err(Error::parse_yaml_err(path, content, &e)),
1206 }
1207}
1208
1209impl Error {
1210 pub fn parse(path: &Path, content: String, err: &serde_json::Error) -> Self {
1215 Error::Parse(Box::new(ParseError::from_json_err(path, content, err)))
1216 }
1217
1218 pub fn parse_yaml_err(path: &Path, content: String, err: &yaml_serde::Error) -> Self {
1221 Error::Parse(Box::new(ParseError::from_yaml_err(path, content, err)))
1222 }
1223
1224 pub fn parse_msg(path: &Path, content: String, message: String) -> Self {
1230 let len = content.len();
1231 let src = miette::NamedSource::new(path.display().to_string(), content);
1232 let span = miette::SourceSpan::new(0.into(), len.min(1));
1233 Error::Parse(Box::new(ParseError {
1234 path: path.to_path_buf(),
1235 message,
1236 src,
1237 span,
1238 }))
1239 }
1240}
1241
1242fn line_col_to_byte_offset(content: &str, line: usize, column: usize) -> usize {
1246 if line == 0 {
1247 return 0;
1248 }
1249 let mut offset = 0usize;
1250 for (i, l) in content.split_inclusive('\n').enumerate() {
1251 if i + 1 == line {
1252 return (offset + column.saturating_sub(1)).min(content.len());
1253 }
1254 offset += l.len();
1255 }
1256 content.len()
1257}
1258
1259#[cfg(test)]
1260mod tests {
1261 use super::*;
1262
1263 fn parse(json: &str) -> PackageJson {
1264 serde_json::from_str(json).unwrap()
1265 }
1266
1267 #[test]
1270 fn npm_package_env_matches_pnpm_allowlist() {
1271 let pkg = parse(
1272 r#"{
1273 "name": "@scope/envprobe",
1274 "version": "4.5.6",
1275 "description": "ignored",
1276 "main": "index.js",
1277 "type": "module",
1278 "private": true,
1279 "os": ["darwin"],
1280 "engines": { "node": ">=18", "npm": ">=9" },
1281 "config": { "port": "8080", "nested": { "deep-key": "x" } },
1282 "bin": { "probe": "./cli.js" }
1283 }"#,
1284 );
1285 let env: std::collections::BTreeMap<String, String> =
1286 pkg.npm_package_env().into_iter().collect();
1287
1288 assert_eq!(
1289 env.get("npm_package_name").map(String::as_str),
1290 Some("@scope/envprobe")
1291 );
1292 assert_eq!(
1293 env.get("npm_package_version").map(String::as_str),
1294 Some("4.5.6")
1295 );
1296 assert_eq!(
1297 env.get("npm_package_engines_node").map(String::as_str),
1298 Some(">=18")
1299 );
1300 assert_eq!(
1301 env.get("npm_package_engines_npm").map(String::as_str),
1302 Some(">=9")
1303 );
1304 assert_eq!(
1305 env.get("npm_package_config_port").map(String::as_str),
1306 Some("8080")
1307 );
1308 assert_eq!(
1310 env.get("npm_package_config_nested_deep_key")
1311 .map(String::as_str),
1312 Some("x")
1313 );
1314 assert_eq!(
1315 env.get("npm_package_bin_probe").map(String::as_str),
1316 Some("./cli.js")
1317 );
1318 for absent in [
1320 "npm_package_description",
1321 "npm_package_main",
1322 "npm_package_type",
1323 "npm_package_private",
1324 "npm_package_os_0",
1325 ] {
1326 assert!(!env.contains_key(absent), "{absent} should not be exported");
1327 }
1328 }
1329
1330 #[test]
1334 fn npm_package_env_string_bin_is_unsuffixed() {
1335 let pkg = parse(r#"{ "name": "@scope/tool", "version": "1.0.0", "bin": "./cli.js" }"#);
1336 let env: std::collections::BTreeMap<String, String> =
1337 pkg.npm_package_env().into_iter().collect();
1338 assert_eq!(
1339 env.get("npm_package_bin").map(String::as_str),
1340 Some("./cli.js")
1341 );
1342 assert!(
1343 !env.keys().any(|k| k.starts_with("npm_package_bin_")),
1344 "string bin must stay unsuffixed: {env:?}"
1345 );
1346 }
1347
1348 #[test]
1354 fn engines_legacy_array_form_parses_as_empty_map() {
1355 let p = parse(r#"{"name":"x","engines":["node >=0.6.0"]}"#);
1356 assert!(p.engines.is_empty());
1357 }
1358
1359 #[test]
1363 fn engines_legacy_string_form_parses_as_empty_map() {
1364 let p = parse(r#"{"name":"x","engines":"node >=0.6.0"}"#);
1365 assert!(p.engines.is_empty());
1366 }
1367
1368 #[test]
1369 fn engines_null_is_treated_as_empty() {
1370 let p = parse(r#"{"name":"x","engines":null}"#);
1371 assert!(p.engines.is_empty());
1372 }
1373
1374 #[test]
1375 fn engines_modern_map_form_still_parses() {
1376 let p = parse(r#"{"name":"x","engines":{"node":">=18.0.0","npm":">=9"}}"#);
1377 assert_eq!(p.engines.get("node").unwrap(), ">=18.0.0");
1378 assert_eq!(p.engines.get("npm").unwrap(), ">=9");
1379 }
1380
1381 #[test]
1382 fn engines_missing_field_is_empty() {
1383 let p = parse(r#"{"name":"x"}"#);
1384 assert!(p.engines.is_empty());
1385 }
1386
1387 #[test]
1391 fn line_col_offset_single_line_col_one() {
1392 assert_eq!(line_col_to_byte_offset("{}", 1, 1), 0);
1393 }
1394
1395 #[test]
1396 fn line_col_offset_multiline_line_two() {
1397 assert_eq!(line_col_to_byte_offset("a\nbc\n", 2, 1), 2);
1399 assert_eq!(line_col_to_byte_offset("a\nbc\n", 2, 2), 3);
1400 }
1401
1402 #[test]
1405 fn line_col_offset_line_zero_returns_zero() {
1406 assert_eq!(line_col_to_byte_offset("any", 0, 5), 0);
1407 }
1408
1409 #[test]
1413 fn line_col_offset_column_past_end_clamps() {
1414 let s = "ab";
1415 assert_eq!(line_col_to_byte_offset(s, 1, 999), s.len());
1416 }
1417
1418 #[test]
1421 fn line_col_offset_line_past_end_clamps() {
1422 let s = "a\nb";
1423 assert_eq!(line_col_to_byte_offset(s, 10, 1), s.len());
1424 }
1425
1426 #[test]
1429 fn line_col_offset_no_trailing_newline() {
1430 let s = "a\nbc";
1431 assert_eq!(line_col_to_byte_offset(s, 2, 2), 3);
1432 }
1433
1434 #[test]
1440 fn parse_error_eof_span_stays_in_bounds() {
1441 let path = Path::new("pkg.json");
1442 let content = r#"{"name":"#.to_string();
1443 let json_err: serde_json::Error = serde_json::from_str::<serde_json::Value>(&content)
1444 .expect_err("truncated JSON must fail");
1445 let Error::Parse(pe) = Error::parse(path, content.clone(), &json_err) else {
1446 panic!("Error::parse must produce Parse variant");
1447 };
1448 let offset: usize = pe.span.offset();
1449 let len: usize = pe.span.len();
1450 assert!(
1451 offset + len <= content.len(),
1452 "span [{offset}, {}) exceeds content.len() {}",
1453 offset + len,
1454 content.len()
1455 );
1456 }
1457
1458 #[test]
1462 fn parse_yaml_attaches_source_span() {
1463 let path = Path::new("pnpm-workspace.yaml");
1464 let content = "packages:\n\t- pkg\n".to_string();
1467 let res: Result<yaml_serde::Value, Error> = parse_yaml(path, content.clone());
1468 let Err(Error::Parse(pe)) = res else {
1469 panic!("parse_yaml must produce Parse variant on malformed input");
1470 };
1471 let offset: usize = pe.span.offset();
1472 let len: usize = pe.span.len();
1473 assert!(offset + len <= content.len());
1474 assert_eq!(pe.path, path);
1475 }
1476
1477 #[test]
1481 fn parse_yaml_err_without_location_falls_back_to_empty_span() {
1482 let path = Path::new("pnpm-workspace.yaml");
1483 let content = String::new();
1484 let yaml_err: yaml_serde::Error =
1485 yaml_serde::from_value::<BTreeMap<String, String>>(yaml_serde::Value::Bool(true))
1486 .expect_err("bool cannot coerce to a map");
1487 assert!(yaml_err.location().is_none());
1488 let Error::Parse(pe) = Error::parse_yaml_err(path, content, &yaml_err) else {
1489 panic!("parse_yaml_err must produce Parse variant");
1490 };
1491 assert_eq!(pe.span.offset(), 0);
1492 assert_eq!(pe.span.len(), 0);
1493 }
1494
1495 #[test]
1496 fn engines_map_drops_non_string_values() {
1497 let p = parse(r#"{"name":"x","engines":{"node":">=18","weird":null,"n":42}}"#);
1500 assert_eq!(p.engines.get("node").unwrap(), ">=18");
1501 assert!(!p.engines.contains_key("weird"));
1502 assert!(!p.engines.contains_key("n"));
1503 }
1504
1505 #[test]
1510 fn scripts_non_string_entries_are_dropped() {
1511 let p = parse(
1512 r#"{
1513 "name":"firefox-profile",
1514 "scripts": {
1515 "test": "grunt travis",
1516 "blanket": { "pattern": ["/lib/firefox_profile"] }
1517 }
1518 }"#,
1519 );
1520 assert_eq!(
1521 p.scripts.get("test").map(String::as_str),
1522 Some("grunt travis")
1523 );
1524 assert!(!p.scripts.contains_key("blanket"));
1525 }
1526
1527 #[test]
1528 fn scripts_null_is_treated_as_empty() {
1529 let p = parse(r#"{"name":"x","scripts":null}"#);
1530 assert!(p.scripts.is_empty());
1531 }
1532
1533 #[test]
1534 fn scripts_non_object_value_is_treated_as_empty() {
1535 let p = parse(r#"{"name":"x","scripts":"oops"}"#);
1539 assert!(p.scripts.is_empty());
1540 }
1541
1542 #[test]
1543 fn selector_key_filter_accepts_valid_forms() {
1544 assert!(is_valid_selector_key("lodash"));
1545 assert!(is_valid_selector_key("@babel/core"));
1546 assert!(is_valid_selector_key("foo>bar"));
1547 assert!(is_valid_selector_key("**/foo"));
1548 assert!(is_valid_selector_key("lodash@<4.17.21"));
1549 assert!(is_valid_selector_key("a@1>b@<2"));
1550 }
1551
1552 #[test]
1553 fn selector_key_filter_rejects_empty() {
1554 assert!(!is_valid_selector_key(""));
1555 }
1556
1557 #[test]
1558 fn overrides_map_collects_top_level() {
1559 let p = parse(r#"{"overrides": {"lodash": "4.17.21"}}"#);
1560 assert_eq!(p.overrides_map().get("lodash").unwrap(), "4.17.21");
1561 }
1562
1563 #[test]
1564 fn overrides_map_top_level_wins_over_pnpm_and_resolutions() {
1565 let p = parse(
1566 r#"{
1567 "resolutions": {"lodash": "1.0.0"},
1568 "pnpm": {"overrides": {"lodash": "2.0.0"}},
1569 "overrides": {"lodash": "3.0.0"}
1570 }"#,
1571 );
1572 assert_eq!(p.overrides_map().get("lodash").unwrap(), "3.0.0");
1573 }
1574
1575 #[test]
1576 fn overrides_map_merges_disjoint_keys() {
1577 let p = parse(
1578 r#"{
1579 "resolutions": {"a": "1"},
1580 "pnpm": {"overrides": {"b": "2"}},
1581 "overrides": {"c": "3"}
1582 }"#,
1583 );
1584 let m = p.overrides_map();
1585 assert_eq!(m.get("a").unwrap(), "1");
1586 assert_eq!(m.get("b").unwrap(), "2");
1587 assert_eq!(m.get("c").unwrap(), "3");
1588 }
1589
1590 #[test]
1591 fn overrides_map_preserves_advanced_selector_keys() {
1592 let p = parse(
1595 r#"{
1596 "overrides": {
1597 "lodash": "4.17.21",
1598 "foo>bar": "1.0.0",
1599 "**/baz": "1.0.0",
1600 "qux@<2": "1.0.0"
1601 }
1602 }"#,
1603 );
1604 let m = p.overrides_map();
1605 assert_eq!(m.len(), 4);
1606 assert!(m.contains_key("lodash"));
1607 assert!(m.contains_key("foo>bar"));
1608 assert!(m.contains_key("**/baz"));
1609 assert!(m.contains_key("qux@<2"));
1610 }
1611
1612 #[test]
1613 fn overrides_map_supports_npm_alias_value() {
1614 let p = parse(r#"{"overrides": {"foo": "npm:bar@^2"}}"#);
1615 assert_eq!(p.overrides_map().get("foo").unwrap(), "npm:bar@^2");
1616 }
1617
1618 #[test]
1619 fn package_extensions_top_level_wins_over_pnpm() {
1620 let p = parse(
1621 r#"{
1622 "pnpm": {"packageExtensions": {"foo": {"dependencies": {"a": "1"}}}},
1623 "packageExtensions": {"foo": {"dependencies": {"a": "2"}}}
1624 }"#,
1625 );
1626 assert_eq!(
1627 p.package_extensions()
1628 .get("foo")
1629 .and_then(|v| v.pointer("/dependencies/a"))
1630 .and_then(|v| v.as_str()),
1631 Some("2")
1632 );
1633 }
1634
1635 #[test]
1636 fn update_ignore_dependencies_merges_top_level_and_pnpm() {
1637 let p = parse(
1638 r#"{
1639 "pnpm": {"updateConfig": {"ignoreDependencies": ["a"]}},
1640 "updateConfig": {"ignoreDependencies": ["b"]}
1641 }"#,
1642 );
1643 assert_eq!(p.update_ignore_dependencies(), vec!["a", "b"]);
1644 }
1645
1646 #[test]
1647 fn overrides_map_skips_object_values() {
1648 let p = parse(r#"{"overrides": {"foo": {"bar": "1.0.0"}}}"#);
1651 assert!(p.overrides_map().is_empty());
1652 }
1653
1654 #[test]
1655 fn resolve_override_refs_substitutes_from_dependencies() {
1656 let p = parse(
1657 r#"{
1658 "dependencies": {"semver": "^7.5.2"},
1659 "overrides": {"semver@<7.5.2": "$semver"}
1660 }"#,
1661 );
1662 let mut m = p.overrides_map();
1663 let unresolved = p.resolve_override_refs(&mut m);
1664 assert!(unresolved.is_empty());
1665 assert_eq!(m.get("semver@<7.5.2").unwrap(), "^7.5.2");
1666 }
1667
1668 #[test]
1669 fn resolve_override_refs_checks_dev_and_optional() {
1670 let p = parse(
1671 r#"{
1672 "devDependencies": {"a": "1.0.0"},
1673 "optionalDependencies": {"b": "2.0.0"},
1674 "overrides": {"a": "$a", "b": "$b"}
1675 }"#,
1676 );
1677 let mut m = p.overrides_map();
1678 let unresolved = p.resolve_override_refs(&mut m);
1679 assert!(unresolved.is_empty());
1680 assert_eq!(m.get("a").unwrap(), "1.0.0");
1681 assert_eq!(m.get("b").unwrap(), "2.0.0");
1682 }
1683
1684 #[test]
1685 fn resolve_override_refs_drops_unresolved() {
1686 let p = parse(
1687 r#"{
1688 "dependencies": {"semver": "^7.5.2"},
1689 "overrides": {
1690 "semver@<7.5.2": "$semver",
1691 "cacheable-request@<10": "$cacheable-request"
1692 }
1693 }"#,
1694 );
1695 let mut m = p.overrides_map();
1696 let unresolved = p.resolve_override_refs(&mut m);
1697 assert_eq!(unresolved, vec!["cacheable-request@<10".to_string()]);
1698 assert_eq!(m.get("semver@<7.5.2").unwrap(), "^7.5.2");
1699 assert!(!m.contains_key("cacheable-request@<10"));
1700 }
1701
1702 #[test]
1703 fn resolve_override_refs_passes_non_dollar_through() {
1704 let p = parse(
1705 r#"{
1706 "dependencies": {"foo": "1.0.0"},
1707 "overrides": {"foo": "2.0.0", "bar": "3.0.0"}
1708 }"#,
1709 );
1710 let mut m = p.overrides_map();
1711 let unresolved = p.resolve_override_refs(&mut m);
1712 assert!(unresolved.is_empty());
1713 assert_eq!(m.get("foo").unwrap(), "2.0.0");
1714 assert_eq!(m.get("bar").unwrap(), "3.0.0");
1715 }
1716
1717 #[test]
1718 fn resolve_override_refs_ignores_peer_dependencies() {
1719 let p = parse(
1723 r#"{
1724 "peerDependencies": {"react": "^18"},
1725 "overrides": {"react": "$react"}
1726 }"#,
1727 );
1728 let mut m = p.overrides_map();
1729 let unresolved = p.resolve_override_refs(&mut m);
1730 assert_eq!(unresolved, vec!["react".to_string()]);
1731 assert!(m.is_empty());
1732 }
1733
1734 #[test]
1735 fn parses_bundled_dependencies_list() {
1736 let p = parse(r#"{"name":"x","bundledDependencies":["foo","bar"]}"#);
1737 let deps = BTreeMap::new();
1738 let names = p.bundled_dependencies.as_ref().unwrap().names(&deps);
1739 assert_eq!(names, vec!["foo", "bar"]);
1740 }
1741
1742 #[test]
1743 fn accepts_legacy_bundle_dependencies_alias() {
1744 let p = parse(r#"{"name":"x","bundleDependencies":["foo"]}"#);
1745 assert!(matches!(
1746 p.bundled_dependencies,
1747 Some(BundledDependencies::List(_))
1748 ));
1749 }
1750
1751 #[test]
1757 fn accepts_both_bundle_and_bundled_dependencies() {
1758 let p = parse(
1759 r#"{"name":"x","bundledDependencies":["canonical"],"bundleDependencies":["legacy"]}"#,
1760 );
1761 let deps = BTreeMap::new();
1762 let names = p.bundled_dependencies.as_ref().unwrap().names(&deps);
1763 assert_eq!(names, vec!["canonical"]);
1764 }
1765
1766 #[test]
1770 fn accepts_both_bundle_and_bundled_dependencies_reverse_order() {
1771 let p = parse(
1772 r#"{"name":"x","bundleDependencies":["legacy"],"bundledDependencies":["canonical"]}"#,
1773 );
1774 let deps = BTreeMap::new();
1775 let names = p.bundled_dependencies.as_ref().unwrap().names(&deps);
1776 assert_eq!(names, vec!["canonical"]);
1777 }
1778
1779 #[test]
1780 fn bundle_true_means_all_production_deps() {
1781 let p =
1782 parse(r#"{"name":"x","dependencies":{"a":"1","b":"2"},"bundledDependencies":true}"#);
1783 let names = p
1784 .bundled_dependencies
1785 .as_ref()
1786 .unwrap()
1787 .names(&p.dependencies);
1788 assert_eq!(names, vec!["a", "b"]);
1789 }
1790
1791 #[test]
1792 fn peer_dependency_rules_accessors_read_nested_pnpm_block() {
1793 let p = parse(
1794 r#"{
1795 "name":"x",
1796 "pnpm": {
1797 "peerDependencyRules": {
1798 "ignoreMissing": ["react", "react-dom"],
1799 "allowAny": ["@types/*"],
1800 "allowedVersions": {
1801 "react": "^18.0.0",
1802 "styled-components>react": "^17.0.0",
1803 "ignored": 42
1804 }
1805 }
1806 }
1807 }"#,
1808 );
1809 assert_eq!(
1810 p.pnpm_peer_dependency_rules_ignore_missing(),
1811 vec!["react".to_string(), "react-dom".to_string()],
1812 );
1813 assert_eq!(
1814 p.pnpm_peer_dependency_rules_allow_any(),
1815 vec!["@types/*".to_string()],
1816 );
1817 let allowed = p.pnpm_peer_dependency_rules_allowed_versions();
1818 assert_eq!(allowed.get("react").map(String::as_str), Some("^18.0.0"));
1819 assert_eq!(
1820 allowed.get("styled-components>react").map(String::as_str),
1821 Some("^17.0.0"),
1822 );
1823 assert!(!allowed.contains_key("ignored"));
1824 }
1825
1826 #[test]
1827 fn peer_dependency_rules_accessors_empty_when_missing() {
1828 let p = parse(r#"{"name":"x"}"#);
1829 assert!(p.pnpm_peer_dependency_rules_ignore_missing().is_empty());
1830 assert!(p.pnpm_peer_dependency_rules_allow_any().is_empty());
1831 assert!(p.pnpm_peer_dependency_rules_allowed_versions().is_empty());
1832 }
1833
1834 #[test]
1837 fn aube_namespace_read_when_pnpm_missing() {
1838 let p = parse(
1839 r#"{
1840 "aube": {
1841 "onlyBuiltDependencies": ["esbuild"],
1842 "neverBuiltDependencies": ["sharp"],
1843 "ignoredOptionalDependencies": ["fsevents"],
1844 "patchedDependencies": {"lodash@4.17.21": "patches/lodash.patch"},
1845 "catalog": {"react": "^18.0.0"},
1846 "catalogs": {"legacy": {"react": "^17.0.0"}},
1847 "supportedArchitectures": {"os": ["linux", "win32"], "cpu": ["x64"]},
1848 "overrides": {"lodash": "4.17.21"},
1849 "packageExtensions": {"foo": {"dependencies": {"a": "1"}}},
1850 "allowedDeprecatedVersions": {"request": "*"},
1851 "peerDependencyRules": {
1852 "ignoreMissing": ["react-native"],
1853 "allowAny": ["@types/*"],
1854 "allowedVersions": {"react": "^18.0.0"}
1855 },
1856 "updateConfig": {"ignoreDependencies": ["typescript"]},
1857 "allowBuilds": {"esbuild": true}
1858 }
1859 }"#,
1860 );
1861 assert_eq!(p.pnpm_only_built_dependencies(), vec!["esbuild"]);
1862 assert_eq!(p.pnpm_never_built_dependencies(), vec!["sharp"]);
1863 assert!(p.pnpm_ignored_optional_dependencies().contains("fsevents"));
1864 assert_eq!(
1865 p.pnpm_patched_dependencies().get("lodash@4.17.21").unwrap(),
1866 "patches/lodash.patch",
1867 );
1868 assert_eq!(p.pnpm_catalog().get("react").unwrap(), "^18.0.0");
1869 assert_eq!(
1870 p.pnpm_catalogs()
1871 .get("legacy")
1872 .and_then(|c| c.get("react"))
1873 .unwrap(),
1874 "^17.0.0",
1875 );
1876 let (os, cpu, libc) = p.pnpm_supported_architectures();
1877 assert_eq!(os, vec!["linux", "win32"]);
1878 assert_eq!(cpu, vec!["x64"]);
1879 assert!(libc.is_empty());
1880 assert_eq!(p.overrides_map().get("lodash").unwrap(), "4.17.21");
1881 assert!(p.package_extensions().contains_key("foo"));
1882 assert_eq!(p.allowed_deprecated_versions().get("request").unwrap(), "*",);
1883 assert_eq!(
1884 p.pnpm_peer_dependency_rules_ignore_missing(),
1885 vec!["react-native".to_string()],
1886 );
1887 assert_eq!(
1888 p.pnpm_peer_dependency_rules_allow_any(),
1889 vec!["@types/*".to_string()],
1890 );
1891 assert_eq!(
1892 p.pnpm_peer_dependency_rules_allowed_versions()
1893 .get("react")
1894 .unwrap(),
1895 "^18.0.0",
1896 );
1897 assert_eq!(p.update_ignore_dependencies(), vec!["typescript"]);
1898 assert!(matches!(
1899 p.pnpm_allow_builds().get("esbuild"),
1900 Some(AllowBuildRaw::Bool(true)),
1901 ));
1902 }
1903
1904 #[test]
1905 fn pnpm_allow_builds_round_trips_string_values_unwrapped() {
1906 let p = parse(
1913 r#"{
1914 "pnpm": {
1915 "allowBuilds": {
1916 "esbuild": "set this to true or false"
1917 }
1918 }
1919 }"#,
1920 );
1921 let map = p.pnpm_allow_builds();
1922 assert_eq!(
1923 map.get("esbuild"),
1924 Some(&AllowBuildRaw::Other(
1925 "set this to true or false".to_string()
1926 )),
1927 );
1928 }
1929
1930 #[test]
1931 fn trusted_dependencies_reads_top_level_bun_format() {
1932 let p = parse(
1933 r#"{
1934 "trustedDependencies": ["esbuild", "sharp", "esbuild"]
1935 }"#,
1936 );
1937 assert_eq!(p.trusted_dependencies(), vec!["esbuild", "sharp"]);
1938 }
1939
1940 #[test]
1941 fn trusted_dependencies_absent_returns_empty() {
1942 let p = parse(r#"{}"#);
1943 assert!(p.trusted_dependencies().is_empty());
1944 }
1945
1946 #[test]
1947 fn trusted_dependencies_wrong_shape_returns_empty() {
1948 let p = parse(r#"{"trustedDependencies": {"esbuild": true}}"#);
1949 assert!(p.trusted_dependencies().is_empty());
1950 }
1951
1952 #[test]
1953 fn bun_patched_dependencies_reads_top_level_field() {
1954 let p = parse(
1955 r#"{
1956 "patchedDependencies": {
1957 "is-number@7.0.0": "patches/is-number.patch",
1958 "ignored@1.0.0": false
1959 }
1960 }"#,
1961 );
1962 let got = p.bun_patched_dependencies();
1963 assert_eq!(
1964 got.get("is-number@7.0.0").unwrap(),
1965 "patches/is-number.patch"
1966 );
1967 assert!(!got.contains_key("ignored@1.0.0"));
1968 }
1969
1970 #[test]
1971 fn aube_overrides_pnpm_on_key_conflict() {
1972 let p = parse(
1975 r#"{
1976 "pnpm": {
1977 "catalog": {"react": "^17.0.0", "lodash": "^4.0.0"},
1978 "patchedDependencies": {"foo@1": "pnpm.patch"},
1979 "allowedDeprecatedVersions": {"request": "^2.0.0"},
1980 "overrides": {"lodash": "pnpm-value"}
1981 },
1982 "aube": {
1983 "catalog": {"react": "^18.0.0"},
1984 "patchedDependencies": {"foo@1": "aube.patch"},
1985 "allowedDeprecatedVersions": {"request": "^3.0.0"},
1986 "overrides": {"lodash": "aube-value"}
1987 }
1988 }"#,
1989 );
1990 let catalog = p.pnpm_catalog();
1991 assert_eq!(catalog.get("react").unwrap(), "^18.0.0");
1992 assert_eq!(catalog.get("lodash").unwrap(), "^4.0.0");
1993 assert_eq!(
1994 p.pnpm_patched_dependencies().get("foo@1").unwrap(),
1995 "aube.patch",
1996 );
1997 assert_eq!(
1998 p.allowed_deprecated_versions().get("request").unwrap(),
1999 "^3.0.0",
2000 );
2001 assert_eq!(p.overrides_map().get("lodash").unwrap(), "aube-value");
2002 }
2003
2004 #[test]
2005 fn top_level_overrides_still_beat_aube_namespace() {
2006 let p = parse(
2009 r#"{
2010 "pnpm": {"overrides": {"lodash": "1"}},
2011 "aube": {"overrides": {"lodash": "2"}},
2012 "overrides": {"lodash": "3"}
2013 }"#,
2014 );
2015 assert_eq!(p.overrides_map().get("lodash").unwrap(), "3");
2016 }
2017
2018 #[test]
2019 fn aube_supported_architectures_merges_with_pnpm() {
2020 let p = parse(
2021 r#"{
2022 "pnpm": {"supportedArchitectures": {"os": ["linux"], "cpu": ["x64"]}},
2023 "aube": {"supportedArchitectures": {"os": ["win32"], "libc": ["glibc"]}}
2024 }"#,
2025 );
2026 let (os, cpu, libc) = p.pnpm_supported_architectures();
2027 assert_eq!(os, vec!["linux", "win32"]);
2028 assert_eq!(cpu, vec!["x64"]);
2029 assert_eq!(libc, vec!["glibc"]);
2030 }
2031
2032 #[test]
2033 fn aube_list_configs_union_with_pnpm() {
2034 let p = parse(
2035 r#"{
2036 "pnpm": {
2037 "onlyBuiltDependencies": ["esbuild"],
2038 "neverBuiltDependencies": ["sharp"],
2039 "ignoredOptionalDependencies": ["fsevents"],
2040 "peerDependencyRules": {
2041 "ignoreMissing": ["react"],
2042 "allowAny": ["@types/a"]
2043 }
2044 },
2045 "aube": {
2046 "onlyBuiltDependencies": ["swc"],
2047 "neverBuiltDependencies": ["node-gyp"],
2048 "ignoredOptionalDependencies": ["dtrace-provider"],
2049 "peerDependencyRules": {
2050 "ignoreMissing": ["react-native"],
2051 "allowAny": ["@types/b"]
2052 }
2053 }
2054 }"#,
2055 );
2056 assert_eq!(p.pnpm_only_built_dependencies(), vec!["esbuild", "swc"]);
2057 assert_eq!(p.pnpm_never_built_dependencies(), vec!["sharp", "node-gyp"]);
2058 let ignored = p.pnpm_ignored_optional_dependencies();
2059 assert!(ignored.contains("fsevents"));
2060 assert!(ignored.contains("dtrace-provider"));
2061 assert_eq!(
2062 p.pnpm_peer_dependency_rules_ignore_missing(),
2063 vec!["react".to_string(), "react-native".to_string()],
2064 );
2065 assert_eq!(
2066 p.pnpm_peer_dependency_rules_allow_any(),
2067 vec!["@types/a".to_string(), "@types/b".to_string()],
2068 );
2069 }
2070
2071 #[test]
2072 fn effective_supported_architectures_unions_manifest_and_workspace() {
2073 let p = parse(
2074 r#"{
2075 "pnpm": {
2076 "supportedArchitectures": {
2077 "os": ["current", "linux"],
2078 "cpu": ["x64"]
2079 }
2080 }
2081 }"#,
2082 );
2083 let ws: workspace::WorkspaceConfig = yaml_serde::from_str(
2084 r#"
2085supportedArchitectures:
2086 os: ["win32"]
2087 cpu: ["x64", "arm64"]
2088 libc: ["glibc"]
2089"#,
2090 )
2091 .unwrap();
2092 let (os, cpu, libc) = effective_supported_architectures(&p, &ws);
2093 assert_eq!(os, vec!["current", "linux", "win32"]);
2095 assert_eq!(cpu, vec!["x64", "arm64"]);
2096 assert_eq!(libc, vec!["glibc"]);
2097 }
2098
2099 #[test]
2100 fn effective_supported_architectures_works_without_either_source() {
2101 let p = parse(r#"{}"#);
2102 let ws = workspace::WorkspaceConfig::default();
2103 let (os, cpu, libc) = effective_supported_architectures(&p, &ws);
2104 assert!(os.is_empty() && cpu.is_empty() && libc.is_empty());
2105 }
2106
2107 #[test]
2108 fn effective_ignored_optional_dependencies_unions_manifest_and_workspace() {
2109 let p = parse(
2110 r#"{
2111 "pnpm": { "ignoredOptionalDependencies": ["fsevents"] }
2112 }"#,
2113 );
2114 let ws: workspace::WorkspaceConfig = yaml_serde::from_str(
2115 r#"
2116ignoredOptionalDependencies:
2117 - dtrace-provider
2118 - fsevents
2119"#,
2120 )
2121 .unwrap();
2122 let merged = effective_ignored_optional_dependencies(&p, &ws);
2123 assert!(merged.contains("fsevents"));
2124 assert!(merged.contains("dtrace-provider"));
2125 assert_eq!(merged.len(), 2);
2126 }
2127
2128 #[test]
2129 fn aube_catalogs_merge_per_key_within_named_catalog() {
2130 let p = parse(
2134 r#"{
2135 "pnpm": {
2136 "catalogs": {
2137 "default": {"react": "^17.0.0", "lodash": "^4.0.0"},
2138 "legacy": {"webpack": "^4.0.0"}
2139 }
2140 },
2141 "aube": {
2142 "catalogs": {
2143 "default": {"react": "^18.0.0", "vite": "^5.0.0"}
2144 }
2145 }
2146 }"#,
2147 );
2148 let cats = p.pnpm_catalogs();
2149 let default = cats.get("default").expect("default catalog present");
2150 assert_eq!(default.get("react").unwrap(), "^18.0.0");
2151 assert_eq!(default.get("lodash").unwrap(), "^4.0.0");
2152 assert_eq!(default.get("vite").unwrap(), "^5.0.0");
2153 let legacy = cats.get("legacy").expect("legacy catalog preserved");
2154 assert_eq!(legacy.get("webpack").unwrap(), "^4.0.0");
2155 }
2156
2157 #[test]
2158 fn aube_list_configs_dedupe_duplicates_across_namespaces() {
2159 let p = parse(
2162 r#"{
2163 "pnpm": {
2164 "onlyBuiltDependencies": ["esbuild", "sharp"],
2165 "neverBuiltDependencies": ["evil"],
2166 "peerDependencyRules": {
2167 "ignoreMissing": ["react"],
2168 "allowAny": ["@types/a"]
2169 }
2170 },
2171 "aube": {
2172 "onlyBuiltDependencies": ["esbuild", "swc"],
2173 "neverBuiltDependencies": ["evil", "node-gyp"],
2174 "peerDependencyRules": {
2175 "ignoreMissing": ["react", "react-native"],
2176 "allowAny": ["@types/a", "@types/b"]
2177 }
2178 }
2179 }"#,
2180 );
2181 assert_eq!(
2182 p.pnpm_only_built_dependencies(),
2183 vec!["esbuild", "sharp", "swc"],
2184 );
2185 assert_eq!(p.pnpm_never_built_dependencies(), vec!["evil", "node-gyp"]);
2186 assert_eq!(
2187 p.pnpm_peer_dependency_rules_ignore_missing(),
2188 vec!["react".to_string(), "react-native".to_string()],
2189 );
2190 assert_eq!(
2191 p.pnpm_peer_dependency_rules_allow_any(),
2192 vec!["@types/a".to_string(), "@types/b".to_string()],
2193 );
2194 }
2195
2196 #[test]
2197 fn aube_update_config_merges_with_pnpm_and_top_level() {
2198 let p = parse(
2199 r#"{
2200 "pnpm": {"updateConfig": {"ignoreDependencies": ["a"]}},
2201 "aube": {"updateConfig": {"ignoreDependencies": ["b"]}},
2202 "updateConfig": {"ignoreDependencies": ["c"]}
2203 }"#,
2204 );
2205 assert_eq!(p.update_ignore_dependencies(), vec!["a", "b", "c"]);
2206 }
2207}