1use serde::{Deserialize, Deserializer, Serialize, de::Error};
2
3#[expect(
7 clippy::disallowed_types,
8 reason = "rustc-hash v2 lacks serde feature — JSON deserialization needs std HashMap"
9)]
10type StdHashMap<K, V> = std::collections::HashMap<K, V>;
11
12fn deserialize_optional_bool_lenient<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
13where
14 D: Deserializer<'de>,
15{
16 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
17 Ok(match value {
18 Some(serde_json::Value::Bool(value)) => Some(value),
19 _ => None,
20 })
21}
22
23fn deserialize_string_array_lenient<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
24where
25 D: Deserializer<'de>,
26{
27 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
28 Ok(match value {
29 Some(serde_json::Value::Array(values)) => values
30 .into_iter()
31 .filter_map(|value| value.as_str().map(str::to_owned))
32 .collect(),
33 _ => Vec::new(),
34 })
35}
36
37fn deserialize_optional_string_lenient<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
38where
39 D: Deserializer<'de>,
40{
41 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
42 Ok(match value {
43 Some(serde_json::Value::String(value)) if !value.trim().is_empty() => Some(value),
44 _ => None,
45 })
46}
47
48fn deserialize_optional_napi_config<'de, D>(deserializer: D) -> Result<Option<NapiConfig>, D::Error>
49where
50 D: Deserializer<'de>,
51{
52 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
53 match value {
54 Some(serde_json::Value::Object(map)) => {
55 serde_json::from_value(serde_json::Value::Object(map))
56 .map(Some)
57 .map_err(D::Error::custom)
58 }
59 _ => Ok(None),
60 }
61}
62
63#[derive(Debug, Clone, Default, Deserialize, Serialize)]
64pub struct PeerDependencyMeta {
65 #[serde(default)]
66 pub optional: bool,
67}
68
69#[derive(Debug, Clone, Default, Deserialize, Serialize)]
74pub struct NapiConfig {
75 #[serde(
77 default,
78 rename = "binaryName",
79 deserialize_with = "deserialize_optional_string_lenient"
80 )]
81 pub binary_name: Option<String>,
82 #[serde(
85 default,
86 rename = "packageName",
87 deserialize_with = "deserialize_optional_string_lenient"
88 )]
89 pub package_name: Option<String>,
90 #[serde(default, deserialize_with = "deserialize_string_array_lenient")]
92 pub targets: Vec<String>,
93}
94
95#[derive(Debug, Clone, Default, Deserialize, Serialize)]
97pub struct PackageJson {
98 #[serde(default)]
101 pub name: Option<String>,
102 #[serde(default, deserialize_with = "deserialize_optional_bool_lenient")]
105 pub private: Option<bool>,
106 #[serde(default)]
108 pub main: Option<String>,
109 #[serde(default)]
111 pub module: Option<String>,
112 #[serde(default)]
114 pub types: Option<String>,
115 #[serde(default)]
117 pub typings: Option<String>,
118 #[serde(default)]
121 pub source: Option<String>,
122 #[serde(default, deserialize_with = "deserialize_string_array_lenient")]
125 pub files: Vec<String>,
126 #[serde(default)]
129 pub browser: Option<serde_json::Value>,
130 #[serde(default)]
133 pub bin: Option<serde_json::Value>,
134 #[serde(default)]
137 pub typescript: Option<serde_json::Value>,
138 #[serde(default)]
142 pub exports: Option<serde_json::Value>,
143 #[serde(default)]
146 pub imports: Option<serde_json::Value>,
147 #[serde(default)]
150 pub dependencies: Option<StdHashMap<String, String>>,
151 #[serde(default, rename = "devDependencies")]
154 pub dev_dependencies: Option<StdHashMap<String, String>>,
155 #[serde(default, rename = "peerDependencies")]
158 pub peer_dependencies: Option<StdHashMap<String, String>>,
159 #[serde(default, rename = "peerDependenciesMeta")]
161 pub peer_dependencies_meta: Option<StdHashMap<String, PeerDependencyMeta>>,
162 #[serde(default, rename = "optionalDependencies")]
165 pub optional_dependencies: Option<StdHashMap<String, String>>,
166 #[serde(default)]
169 pub scripts: Option<StdHashMap<String, String>>,
170 #[serde(default, deserialize_with = "deserialize_optional_napi_config")]
173 pub napi: Option<NapiConfig>,
174 #[serde(default)]
177 pub workspaces: Option<serde_json::Value>,
178 #[serde(
181 default,
182 rename = "packageManager",
183 deserialize_with = "deserialize_optional_string_lenient"
184 )]
185 pub package_manager: Option<String>,
186 #[serde(default)]
189 pub egg: Option<serde_json::Value>,
190 #[serde(default, rename = "eggPlugin")]
192 pub egg_plugin: Option<serde_json::Value>,
193 #[serde(default, rename = "eggModule")]
195 pub egg_module: Option<serde_json::Value>,
196}
197
198impl PackageJson {
199 pub fn load(path: &std::path::Path) -> Result<Self, String> {
205 let content = std::fs::read_to_string(path)
206 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
207 let content = content.trim_start_matches('\u{FEFF}');
208 serde_json::from_str(content)
209 .map_err(|e| format!("Failed to parse {}: {}", path.display(), e))
210 }
211
212 #[must_use]
214 pub fn all_dependency_names(&self) -> Vec<String> {
215 let mut deps = Vec::new();
216 if let Some(d) = &self.dependencies {
217 deps.extend(d.keys().cloned());
218 }
219 if let Some(d) = &self.dev_dependencies {
220 deps.extend(d.keys().cloned());
221 }
222 if let Some(d) = &self.peer_dependencies {
223 deps.extend(d.keys().cloned());
224 }
225 if let Some(d) = &self.optional_dependencies {
226 deps.extend(d.keys().cloned());
227 }
228 deps
229 }
230
231 #[must_use]
233 pub fn production_dependency_names(&self) -> Vec<String> {
234 self.dependencies
235 .as_ref()
236 .map(|d| d.keys().cloned().collect())
237 .unwrap_or_default()
238 }
239
240 #[must_use]
242 pub fn dev_dependency_names(&self) -> Vec<String> {
243 self.dev_dependencies
244 .as_ref()
245 .map(|d| d.keys().cloned().collect())
246 .unwrap_or_default()
247 }
248
249 #[must_use]
251 pub fn optional_dependency_names(&self) -> Vec<String> {
252 self.optional_dependencies
253 .as_ref()
254 .map(|d| d.keys().cloned().collect())
255 .unwrap_or_default()
256 }
257
258 #[must_use]
260 pub fn peer_dependency_names(&self) -> Vec<String> {
261 self.peer_dependencies
262 .as_ref()
263 .map(|deps| deps.keys().cloned().collect())
264 .unwrap_or_default()
265 }
266
267 #[must_use]
269 pub fn peer_dependency_is_optional(&self, dep: &str) -> bool {
270 self.peer_dependencies_meta
271 .as_ref()
272 .and_then(|meta| meta.get(dep))
273 .is_some_and(|meta| meta.optional)
274 }
275
276 #[must_use]
278 pub fn entry_points(&self) -> Vec<String> {
279 let mut entries = Vec::new();
280
281 if let Some(main) = &self.main {
282 entries.push(main.clone());
283 }
284 if let Some(module) = &self.module {
285 entries.push(module.clone());
286 }
287 if let Some(types) = &self.types {
288 entries.push(types.clone());
289 }
290 if let Some(typings) = &self.typings {
291 entries.push(typings.clone());
292 }
293 if let Some(source) = &self.source {
294 entries.push(source.clone());
295 }
296
297 if let Some(browser) = &self.browser {
298 match browser {
299 serde_json::Value::String(s) => entries.push(s.clone()),
300 serde_json::Value::Object(map) => {
301 for v in map.values() {
302 if let serde_json::Value::String(s) = v
303 && (s.starts_with("./") || s.starts_with("../"))
304 {
305 entries.push(s.clone());
306 }
307 }
308 }
309 _ => {}
310 }
311 }
312
313 if let Some(bin) = &self.bin {
314 match bin {
315 serde_json::Value::String(s) => entries.push(s.clone()),
316 serde_json::Value::Object(map) => {
317 for v in map.values() {
318 if let serde_json::Value::String(s) = v {
319 entries.push(s.clone());
320 }
321 }
322 }
323 _ => {}
324 }
325 }
326
327 if let Some(exports) = &self.exports {
328 extract_exports_entries(exports, &mut entries);
329 }
330
331 entries
332 }
333
334 #[must_use]
341 pub fn exports_subdirectories(&self) -> Vec<String> {
342 self.exports
343 .as_ref()
344 .map_or_else(Vec::new, extract_exports_subdirectories)
345 }
346
347 #[must_use]
349 pub fn workspace_patterns(&self) -> Vec<String> {
350 match &self.workspaces {
351 Some(serde_json::Value::Array(arr)) => arr
352 .iter()
353 .filter_map(|v| v.as_str().map(String::from))
354 .collect(),
355 Some(serde_json::Value::Object(obj)) => obj
356 .get("packages")
357 .and_then(|v| v.as_array())
358 .map(|arr| {
359 arr.iter()
360 .filter_map(|v| v.as_str().map(String::from))
361 .collect()
362 })
363 .unwrap_or_default(),
364 _ => Vec::new(),
365 }
366 }
367
368 #[must_use]
376 pub fn local_link_dependencies(&self) -> Vec<(String, String)> {
377 let mut links: Vec<(String, String)> = [
378 &self.dependencies,
379 &self.dev_dependencies,
380 &self.optional_dependencies,
381 ]
382 .into_iter()
383 .flatten()
384 .flatten()
385 .filter_map(|(name, spec)| {
386 let target = spec
387 .strip_prefix("link:")
388 .or_else(|| spec.strip_prefix("file:"))?
389 .trim();
390 (!target.is_empty()).then(|| (name.clone(), target.to_string()))
391 })
392 .collect();
393 links.sort_unstable();
394 links.dedup();
395 links
396 }
397
398 #[must_use]
405 pub fn workspace_alias_target(&self, name: &str) -> Option<&str> {
406 [
407 &self.dependencies,
408 &self.dev_dependencies,
409 &self.optional_dependencies,
410 &self.peer_dependencies,
411 ]
412 .into_iter()
413 .flatten()
414 .find_map(|deps| deps.get(name))
415 .and_then(|spec| workspace_alias_name(spec))
416 }
417
418 #[must_use]
425 pub fn npm_aliases(&self) -> Vec<(String, String)> {
426 let mut aliases: Vec<(String, String)> = [
427 &self.dependencies,
428 &self.dev_dependencies,
429 &self.optional_dependencies,
430 &self.peer_dependencies,
431 ]
432 .into_iter()
433 .flatten()
434 .flat_map(|deps| deps.iter())
435 .filter_map(|(name, spec)| {
436 let target = npm_alias_name(spec)?;
437 (target != name).then(|| (name.clone(), target.to_string()))
438 })
439 .collect();
440 aliases.sort_unstable();
441 aliases.dedup();
442 aliases
443 }
444}
445
446fn workspace_alias_name(spec: &str) -> Option<&str> {
448 let rest = spec.trim().strip_prefix("workspace:")?;
449 let separator = if let Some(scoped) = rest.strip_prefix('@') {
450 scoped.find('@')? + 1
451 } else {
452 rest.find('@')?
453 };
454 valid_package_name(&rest[..separator])
455}
456
457fn npm_alias_name(spec: &str) -> Option<&str> {
460 let rest = spec.trim().strip_prefix("npm:")?;
461 let separator = if let Some(scoped) = rest.strip_prefix('@') {
462 scoped.find('@').map_or(rest.len(), |at| at + 1)
463 } else {
464 rest.find('@').unwrap_or(rest.len())
465 };
466 valid_package_name(&rest[..separator])
467}
468
469fn valid_package_name(name: &str) -> Option<&str> {
471 let is_package_name = !name.is_empty()
472 && !name.starts_with('.')
473 && match name.strip_prefix('@') {
474 Some(scoped) => scoped
475 .split_once('/')
476 .is_some_and(|(scope, base)| !scope.is_empty() && !base.is_empty()),
477 None => !name.contains('/'),
478 };
479 is_package_name.then_some(name)
480}
481
482fn extract_exports_subdirectories(exports: &serde_json::Value) -> Vec<String> {
489 let Some(map) = exports.as_object() else {
490 return Vec::new();
491 };
492
493 let skip_dirs = ["dist", "build", "out", "esm", "cjs", "lib", "node_modules"];
494 let mut dirs = rustc_hash::FxHashSet::default();
495
496 for key in map.keys() {
497 let stripped = key.strip_prefix("./").unwrap_or(key);
498 if let Some(first_segment) = stripped.split('/').next()
499 && !first_segment.is_empty()
500 && first_segment != "."
501 && first_segment != "package.json"
502 && !skip_dirs.contains(&first_segment)
503 {
504 dirs.insert(first_segment.to_owned());
505 }
506 }
507
508 dirs.into_iter().collect()
509}
510
511fn extract_exports_entries(value: &serde_json::Value, entries: &mut Vec<String>) {
513 match value {
514 serde_json::Value::String(s) if s.starts_with("./") || s.starts_with("../") => {
515 entries.push(s.clone());
516 }
517 serde_json::Value::Object(map) => {
518 for v in map.values() {
519 extract_exports_entries(v, entries);
520 }
521 }
522 serde_json::Value::Array(arr) => {
523 for v in arr {
524 extract_exports_entries(v, entries);
525 }
526 }
527 _ => {}
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 #[test]
536 fn package_json_workspace_patterns_array() {
537 let pkg: PackageJson =
538 serde_json::from_str(r#"{"workspaces": ["packages/*", "apps/*"]}"#).unwrap();
539 let patterns = pkg.workspace_patterns();
540 assert_eq!(patterns, vec!["packages/*", "apps/*"]);
541 }
542
543 #[test]
544 fn package_json_workspace_patterns_object() {
545 let pkg: PackageJson =
546 serde_json::from_str(r#"{"workspaces": {"packages": ["packages/*"]}}"#).unwrap();
547 let patterns = pkg.workspace_patterns();
548 assert_eq!(patterns, vec!["packages/*"]);
549 }
550
551 #[test]
552 fn package_json_workspace_patterns_none() {
553 let pkg: PackageJson = serde_json::from_str(r#"{"name": "test"}"#).unwrap();
554 let patterns = pkg.workspace_patterns();
555 assert!(patterns.is_empty());
556 }
557
558 #[test]
559 fn package_json_workspace_patterns_empty_array() {
560 let pkg: PackageJson = serde_json::from_str(r#"{"workspaces": []}"#).unwrap();
561 let patterns = pkg.workspace_patterns();
562 assert!(patterns.is_empty());
563 }
564
565 #[test]
566 fn package_json_load_valid() {
567 let temp_dir = std::env::temp_dir().join("fallow-test-pkg-json");
568 let _ = std::fs::create_dir_all(&temp_dir);
569 let pkg_path = temp_dir.join("package.json");
570 std::fs::write(&pkg_path, r#"{"name": "test", "main": "index.js"}"#).unwrap();
571
572 let pkg = PackageJson::load(&pkg_path).unwrap();
573 assert_eq!(pkg.name, Some("test".to_string()));
574 assert_eq!(pkg.main, Some("index.js".to_string()));
575
576 let _ = std::fs::remove_dir_all(&temp_dir);
577 }
578
579 #[test]
580 fn package_json_private_non_bool_values_are_ignored() {
581 for raw in [
582 r#"{"private": "true"}"#,
583 r#"{"private": 1}"#,
584 r#"{"private": null}"#,
585 ] {
586 let pkg: PackageJson = serde_json::from_str(raw).unwrap();
587 assert_eq!(pkg.private, None);
588 }
589
590 let pkg: PackageJson = serde_json::from_str(r#"{"private": true}"#).unwrap();
591 assert_eq!(pkg.private, Some(true));
592
593 let pkg: PackageJson = serde_json::from_str(r#"{"private": false}"#).unwrap();
594 assert_eq!(pkg.private, Some(false));
595 }
596
597 #[test]
598 fn package_json_files_preserves_string_array_values() {
599 let pkg: PackageJson =
600 serde_json::from_str(r#"{"files": ["index.js", "template-*", "dist"]}"#).unwrap();
601 assert_eq!(pkg.files, vec!["index.js", "template-*", "dist"]);
602 }
603
604 #[test]
605 fn package_json_files_missing_or_unexpected_shapes_are_ignored() {
606 let pkg: PackageJson = serde_json::from_str(r#"{"name": "pkg"}"#).unwrap();
607 assert!(pkg.files.is_empty());
608
609 let pkg: PackageJson = serde_json::from_str(r#"{"files": "dist"}"#).unwrap();
610 assert!(pkg.files.is_empty());
611
612 let pkg: PackageJson =
613 serde_json::from_str(r#"{"files": ["template-*", 42, false, null]}"#).unwrap();
614 assert_eq!(pkg.files, vec!["template-*"]);
615 }
616
617 #[test]
618 fn package_json_napi_config_preserves_current_string_fields() {
619 let pkg: PackageJson = serde_json::from_str(
620 r#"{
621 "napi": {
622 "binaryName": "srcmap-codec",
623 "packageName": "@srcmap/codec",
624 "targets": [
625 "aarch64-apple-darwin",
626 "x86_64-unknown-linux-gnu"
627 ]
628 }
629 }"#,
630 )
631 .unwrap();
632
633 let napi = pkg.napi.expect("napi config");
634 assert_eq!(napi.binary_name.as_deref(), Some("srcmap-codec"));
635 assert_eq!(napi.package_name.as_deref(), Some("@srcmap/codec"));
636 assert_eq!(
637 napi.targets,
638 vec![
639 "aarch64-apple-darwin".to_string(),
640 "x86_64-unknown-linux-gnu".to_string()
641 ]
642 );
643 }
644
645 #[test]
646 fn package_json_napi_config_ignores_unexpected_shapes() {
647 let pkg: PackageJson = serde_json::from_str(r#"{"napi": "enabled"}"#).unwrap();
648 assert!(pkg.napi.is_none());
649
650 let pkg: PackageJson = serde_json::from_str(
651 r#"{
652 "napi": {
653 "binaryName": 42,
654 "packageName": "",
655 "targets": ["x86_64-apple-darwin", false, null]
656 }
657 }"#,
658 )
659 .unwrap();
660
661 let napi = pkg.napi.expect("napi config");
662 assert!(napi.binary_name.is_none());
663 assert!(napi.package_name.is_none());
664 assert_eq!(napi.targets, vec!["x86_64-apple-darwin"]);
665 }
666
667 #[test]
668 fn package_json_load_missing_file() {
669 let result = PackageJson::load(std::path::Path::new("/nonexistent/package.json"));
670 assert!(result.is_err());
671 }
672
673 #[test]
674 fn package_json_entry_points_combined() {
675 let pkg: PackageJson = serde_json::from_str(
676 r#"{
677 "main": "dist/index.js",
678 "module": "dist/index.mjs",
679 "types": "dist/index.d.ts",
680 "typings": "dist/types.d.ts"
681 }"#,
682 )
683 .unwrap();
684 let entries = pkg.entry_points();
685 assert_eq!(entries.len(), 4);
686 assert!(entries.contains(&"dist/index.js".to_string()));
687 assert!(entries.contains(&"dist/index.mjs".to_string()));
688 assert!(entries.contains(&"dist/index.d.ts".to_string()));
689 assert!(entries.contains(&"dist/types.d.ts".to_string()));
690 }
691
692 #[test]
693 fn package_json_exports_nested() {
694 let pkg: PackageJson = serde_json::from_str(
695 r#"{
696 "exports": {
697 ".": {
698 "import": "./dist/index.mjs",
699 "require": "./dist/index.cjs"
700 },
701 "./utils": {
702 "import": "./dist/utils.mjs"
703 }
704 }
705 }"#,
706 )
707 .unwrap();
708 let entries = pkg.entry_points();
709 assert!(entries.contains(&"./dist/index.mjs".to_string()));
710 assert!(entries.contains(&"./dist/index.cjs".to_string()));
711 assert!(entries.contains(&"./dist/utils.mjs".to_string()));
712 }
713
714 #[test]
715 fn package_json_exports_array() {
716 let pkg: PackageJson = serde_json::from_str(
717 r#"{
718 "exports": {
719 ".": ["./dist/index.mjs", "./dist/index.cjs"]
720 }
721 }"#,
722 )
723 .unwrap();
724 let entries = pkg.entry_points();
725 assert!(entries.contains(&"./dist/index.mjs".to_string()));
726 assert!(entries.contains(&"./dist/index.cjs".to_string()));
727 }
728
729 #[test]
730 fn extract_exports_ignores_non_relative() {
731 let pkg: PackageJson = serde_json::from_str(
732 r#"{
733 "exports": {
734 ".": "not-a-relative-path"
735 }
736 }"#,
737 )
738 .unwrap();
739 let entries = pkg.entry_points();
740 assert!(entries.is_empty());
741 }
742
743 #[test]
744 fn package_json_source_field() {
745 let pkg: PackageJson = serde_json::from_str(
746 r#"{
747 "main": "dist/index.js",
748 "source": "src/index.ts"
749 }"#,
750 )
751 .unwrap();
752 let entries = pkg.entry_points();
753 assert!(entries.contains(&"src/index.ts".to_string()));
754 assert!(entries.contains(&"dist/index.js".to_string()));
755 }
756
757 #[test]
758 fn package_json_browser_field_string() {
759 let pkg: PackageJson = serde_json::from_str(
760 r#"{
761 "browser": "./dist/browser.js"
762 }"#,
763 )
764 .unwrap();
765 let entries = pkg.entry_points();
766 assert!(entries.contains(&"./dist/browser.js".to_string()));
767 }
768
769 #[test]
770 fn package_json_browser_field_object() {
771 let pkg: PackageJson = serde_json::from_str(
772 r#"{
773 "browser": {
774 "./server.js": "./browser.js",
775 "module-name": false
776 }
777 }"#,
778 )
779 .unwrap();
780 let entries = pkg.entry_points();
781 assert!(entries.contains(&"./browser.js".to_string()));
782 assert_eq!(entries.len(), 1);
783 }
784
785 #[test]
786 fn package_json_exports_string() {
787 let pkg: PackageJson = serde_json::from_str(r#"{"exports": "./dist/index.js"}"#).unwrap();
788 let entries = pkg.entry_points();
789 assert_eq!(entries, vec!["./dist/index.js"]);
790 }
791
792 #[test]
793 fn package_json_workspace_patterns_object_with_nohoist() {
794 let pkg: PackageJson = serde_json::from_str(
795 r#"{
796 "workspaces": {
797 "packages": ["packages/*", "apps/*"],
798 "nohoist": ["**/react-native"]
799 }
800 }"#,
801 )
802 .unwrap();
803 let patterns = pkg.workspace_patterns();
804 assert_eq!(patterns, vec!["packages/*", "apps/*"]);
805 }
806
807 #[test]
808 fn package_json_missing_optional_fields() {
809 let pkg: PackageJson = serde_json::from_str(r"{}").unwrap();
810 assert!(pkg.name.is_none());
811 assert!(pkg.main.is_none());
812 assert!(pkg.module.is_none());
813 assert!(pkg.types.is_none());
814 assert!(pkg.typings.is_none());
815 assert!(pkg.source.is_none());
816 assert!(pkg.browser.is_none());
817 assert!(pkg.bin.is_none());
818 assert!(pkg.exports.is_none());
819 assert!(pkg.dependencies.is_none());
820 assert!(pkg.dev_dependencies.is_none());
821 assert!(pkg.peer_dependencies.is_none());
822 assert!(pkg.peer_dependencies_meta.is_none());
823 assert!(pkg.optional_dependencies.is_none());
824 assert!(pkg.scripts.is_none());
825 assert!(pkg.workspaces.is_none());
826 assert!(pkg.entry_points().is_empty());
827 assert!(pkg.workspace_patterns().is_empty());
828 assert!(pkg.all_dependency_names().is_empty());
829 }
830
831 #[test]
832 fn package_json_all_dependency_names() {
833 let pkg: PackageJson = serde_json::from_str(
834 r#"{
835 "dependencies": {"react": "^18", "react-dom": "^18"},
836 "devDependencies": {"typescript": "^5"},
837 "peerDependencies": {"node": ">=18"},
838 "optionalDependencies": {"fsevents": "^2"}
839 }"#,
840 )
841 .unwrap();
842 let deps = pkg.all_dependency_names();
843 assert_eq!(deps.len(), 5);
844 assert!(deps.contains(&"react".to_string()));
845 assert!(deps.contains(&"react-dom".to_string()));
846 assert!(deps.contains(&"typescript".to_string()));
847 assert!(deps.contains(&"node".to_string()));
848 assert!(deps.contains(&"fsevents".to_string()));
849 }
850
851 #[test]
852 fn package_json_production_dependency_names() {
853 let pkg: PackageJson = serde_json::from_str(
854 r#"{
855 "dependencies": {"react": "^18"},
856 "devDependencies": {"typescript": "^5"}
857 }"#,
858 )
859 .unwrap();
860 let prod = pkg.production_dependency_names();
861 assert_eq!(prod, vec!["react"]);
862 let dev = pkg.dev_dependency_names();
863 assert_eq!(dev, vec!["typescript"]);
864 }
865
866 #[test]
867 fn package_json_bin_field_string() {
868 let pkg: PackageJson = serde_json::from_str(r#"{"bin": "./cli.js"}"#).unwrap();
869 let entries = pkg.entry_points();
870 assert!(entries.contains(&"./cli.js".to_string()));
871 }
872
873 #[test]
874 fn package_json_bin_field_object() {
875 let pkg: PackageJson = serde_json::from_str(
876 r#"{"bin": {"my-cli": "./bin/cli.js", "my-tool": "./bin/tool.js"}}"#,
877 )
878 .unwrap();
879 let entries = pkg.entry_points();
880 assert!(entries.contains(&"./bin/cli.js".to_string()));
881 assert!(entries.contains(&"./bin/tool.js".to_string()));
882 }
883
884 #[test]
885 fn package_json_exports_deeply_nested() {
886 let pkg: PackageJson = serde_json::from_str(
887 r#"{
888 "exports": {
889 ".": {
890 "node": {
891 "import": "./dist/node.mjs",
892 "require": "./dist/node.cjs"
893 },
894 "browser": {
895 "import": "./dist/browser.mjs"
896 }
897 }
898 }
899 }"#,
900 )
901 .unwrap();
902 let entries = pkg.entry_points();
903 assert_eq!(entries.len(), 3);
904 assert!(entries.contains(&"./dist/node.mjs".to_string()));
905 assert!(entries.contains(&"./dist/node.cjs".to_string()));
906 assert!(entries.contains(&"./dist/browser.mjs".to_string()));
907 }
908
909 #[test]
910 fn package_json_peer_deps_only() {
911 let pkg: PackageJson =
912 serde_json::from_str(r#"{"peerDependencies": {"react": "^18", "react-dom": "^18"}}"#)
913 .unwrap();
914 let all = pkg.all_dependency_names();
915 assert_eq!(all.len(), 2);
916 assert!(all.contains(&"react".to_string()));
917 assert!(all.contains(&"react-dom".to_string()));
918
919 assert!(pkg.production_dependency_names().is_empty());
920 assert!(pkg.dev_dependency_names().is_empty());
921 }
922
923 #[test]
924 fn package_json_peer_dependency_names_include_optional_peers() {
925 let pkg: PackageJson = serde_json::from_str(
926 r#"{
927 "peerDependencies": {"react": "^18", "typescript": "^5"},
928 "peerDependenciesMeta": {"typescript": {"optional": true}}
929 }"#,
930 )
931 .unwrap();
932 let mut names = pkg.peer_dependency_names();
933 names.sort_unstable();
934 assert_eq!(names, vec!["react", "typescript"]);
935 }
936
937 #[test]
938 fn package_json_optional_deps_in_all_names() {
939 let pkg: PackageJson =
940 serde_json::from_str(r#"{"optionalDependencies": {"fsevents": "^2"}}"#).unwrap();
941 let all = pkg.all_dependency_names();
942 assert!(all.contains(&"fsevents".to_string()));
943 }
944
945 #[test]
946 fn package_json_browser_array_ignored() {
947 let pkg: PackageJson =
948 serde_json::from_str(r#"{"browser": ["./a.js", "./b.js"]}"#).unwrap();
949 let entries = pkg.entry_points();
950 assert!(
951 entries.is_empty(),
952 "array browser field should not produce entries"
953 );
954 }
955
956 #[test]
957 fn package_json_browser_object_non_relative_skipped() {
958 let pkg: PackageJson = serde_json::from_str(
959 r#"{"browser": {"crypto": false, "./local.js": "./browser-local.js"}}"#,
960 )
961 .unwrap();
962 let entries = pkg.entry_points();
963 assert_eq!(entries.len(), 1);
964 assert!(entries.contains(&"./browser-local.js".to_string()));
965 }
966
967 #[test]
968 fn package_json_exports_null_value() {
969 let pkg: PackageJson =
970 serde_json::from_str(r#"{"exports": {".": "./dist/index.js", "./internal": null}}"#)
971 .unwrap();
972 let entries = pkg.entry_points();
973 assert_eq!(entries.len(), 1);
974 assert!(entries.contains(&"./dist/index.js".to_string()));
975 }
976
977 #[test]
978 fn package_json_exports_empty_object() {
979 let pkg: PackageJson = serde_json::from_str(r#"{"exports": {}}"#).unwrap();
980 let entries = pkg.entry_points();
981 assert!(entries.is_empty());
982 }
983
984 #[test]
985 fn package_json_workspace_patterns_string_value_ignored() {
986 let pkg: PackageJson = serde_json::from_str(r#"{"workspaces": "packages/*"}"#).unwrap();
987 let patterns = pkg.workspace_patterns();
988 assert!(patterns.is_empty());
989 }
990
991 #[test]
992 fn package_json_workspace_patterns_object_missing_packages() {
993 let pkg: PackageJson =
994 serde_json::from_str(r#"{"workspaces": {"nohoist": ["**/react-native"]}}"#).unwrap();
995 let patterns = pkg.workspace_patterns();
996 assert!(patterns.is_empty());
997 }
998
999 #[test]
1000 fn package_json_load_invalid_json() {
1001 let temp_dir = std::env::temp_dir().join("fallow-test-invalid-pkg-json");
1002 let _ = std::fs::create_dir_all(&temp_dir);
1003 let pkg_path = temp_dir.join("package.json");
1004 std::fs::write(&pkg_path, "{ not valid json }").unwrap();
1005
1006 let result = PackageJson::load(&pkg_path);
1007 assert!(result.is_err());
1008 let err = result.unwrap_err();
1009 assert!(err.contains("Failed to parse"), "got: {err}");
1010
1011 let _ = std::fs::remove_dir_all(&temp_dir);
1012 }
1013
1014 #[test]
1015 fn package_json_bin_object_non_string_values_skipped() {
1016 let pkg: PackageJson =
1017 serde_json::from_str(r#"{"bin": {"cli": "./bin/cli.js", "bad": 42}}"#).unwrap();
1018 let entries = pkg.entry_points();
1019 assert_eq!(entries.len(), 1);
1020 assert!(entries.contains(&"./bin/cli.js".to_string()));
1021 }
1022
1023 #[test]
1024 fn package_json_default() {
1025 let pkg = PackageJson::default();
1026 assert!(pkg.name.is_none());
1027 assert!(pkg.main.is_none());
1028 assert!(pkg.entry_points().is_empty());
1029 assert!(pkg.all_dependency_names().is_empty());
1030 assert!(pkg.workspace_patterns().is_empty());
1031 }
1032
1033 #[test]
1034 fn exports_subdirectories_preact_style() {
1035 let pkg: PackageJson = serde_json::from_str(
1036 r#"{
1037 "exports": {
1038 ".": "./dist/index.js",
1039 "./compat": { "import": "./compat/dist/compat.mjs" },
1040 "./hooks": { "import": "./hooks/dist/hooks.mjs" },
1041 "./debug": { "import": "./debug/dist/debug.mjs" },
1042 "./jsx-runtime": { "import": "./jsx-runtime/dist/jsx.mjs" },
1043 "./package.json": "./package.json"
1044 }
1045 }"#,
1046 )
1047 .unwrap();
1048 let mut dirs = pkg.exports_subdirectories();
1049 dirs.sort();
1050 assert_eq!(dirs, vec!["compat", "debug", "hooks", "jsx-runtime"]);
1051 }
1052
1053 #[test]
1054 fn exports_subdirectories_skips_dist_dirs() {
1055 let pkg: PackageJson = serde_json::from_str(
1056 r#"{
1057 "exports": {
1058 "./dist/index.js": "./dist/index.js",
1059 "./build/bundle.js": "./build/bundle.js",
1060 "./lib/utils": "./lib/utils.js",
1061 "./compat": "./compat/index.js"
1062 }
1063 }"#,
1064 )
1065 .unwrap();
1066 let dirs = pkg.exports_subdirectories();
1067 assert_eq!(dirs, vec!["compat"]);
1068 }
1069
1070 #[test]
1071 fn exports_subdirectories_no_exports() {
1072 let pkg: PackageJson = serde_json::from_str(r#"{"main": "index.js"}"#).unwrap();
1073 assert!(pkg.exports_subdirectories().is_empty());
1074 }
1075
1076 #[test]
1077 fn exports_subdirectories_dot_only() {
1078 let pkg: PackageJson =
1079 serde_json::from_str(r#"{"exports": {".": "./dist/index.js"}}"#).unwrap();
1080 assert!(pkg.exports_subdirectories().is_empty());
1081 }
1082
1083 #[test]
1084 fn npm_aliases_read_the_aliased_package_name() {
1085 let pkg: PackageJson = serde_json::from_str(
1086 r#"{
1087 "dependencies": {"plain": "npm:left-pad@^1.3.0", "range": "^1.0.0"},
1088 "devDependencies": {
1089 "@typescript/native": "npm:typescript@~7.0.2",
1090 "typescript": "npm:@typescript/typescript6@^6.0.2",
1091 "bare": "npm:eslint",
1092 "same": "npm:same@1.0.0",
1093 "path": "npm:./local",
1094 "bad-scope": "npm:@scope@1.0.0",
1095 "workspace": "workspace:@scope/core@*"
1096 }
1097 }"#,
1098 )
1099 .unwrap();
1100 assert_eq!(
1101 pkg.npm_aliases(),
1102 [
1103 ("@typescript/native", "typescript"),
1104 ("bare", "eslint"),
1105 ("plain", "left-pad"),
1106 ("typescript", "@typescript/typescript6"),
1107 ]
1108 .map(|(alias, target)| (alias.to_string(), target.to_string()))
1109 );
1110 }
1111
1112 #[test]
1113 fn workspace_alias_target_reads_the_aliased_package_name() {
1114 let pkg: PackageJson = serde_json::from_str(
1115 r#"{
1116 "dependencies": {
1117 "vite": "workspace:@scope/core@*",
1118 "plain": "workspace:other@^1.0.0",
1119 "range": "workspace:*",
1120 "caret": "workspace:^1.0.0",
1121 "path": "workspace:../core",
1122 "npm": "npm:@scope/core@1.0.0",
1123 "bad-scope": "workspace:@scope@1.0.0"
1124 },
1125 "devDependencies": {"tool": "workspace:@scope/tool@workspace:*"}
1126 }"#,
1127 )
1128 .unwrap();
1129 assert_eq!(pkg.workspace_alias_target("vite"), Some("@scope/core"));
1130 assert_eq!(pkg.workspace_alias_target("plain"), Some("other"));
1131 assert_eq!(pkg.workspace_alias_target("tool"), Some("@scope/tool"));
1132 for name in ["range", "caret", "path", "npm", "bad-scope", "missing"] {
1133 assert_eq!(pkg.workspace_alias_target(name), None, "{name}");
1134 }
1135 }
1136}