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)]
138 pub exports: Option<serde_json::Value>,
139 #[serde(default)]
142 pub imports: Option<serde_json::Value>,
143 #[serde(default)]
146 pub dependencies: Option<StdHashMap<String, String>>,
147 #[serde(default, rename = "devDependencies")]
150 pub dev_dependencies: Option<StdHashMap<String, String>>,
151 #[serde(default, rename = "peerDependencies")]
154 pub peer_dependencies: Option<StdHashMap<String, String>>,
155 #[serde(default, rename = "peerDependenciesMeta")]
157 pub peer_dependencies_meta: Option<StdHashMap<String, PeerDependencyMeta>>,
158 #[serde(default, rename = "optionalDependencies")]
161 pub optional_dependencies: Option<StdHashMap<String, String>>,
162 #[serde(default)]
165 pub scripts: Option<StdHashMap<String, String>>,
166 #[serde(default, deserialize_with = "deserialize_optional_napi_config")]
169 pub napi: Option<NapiConfig>,
170 #[serde(default)]
173 pub workspaces: Option<serde_json::Value>,
174 #[serde(
177 default,
178 rename = "packageManager",
179 deserialize_with = "deserialize_optional_string_lenient"
180 )]
181 pub package_manager: Option<String>,
182}
183
184impl PackageJson {
185 pub fn load(path: &std::path::Path) -> Result<Self, String> {
191 let content = std::fs::read_to_string(path)
192 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
193 let content = content.trim_start_matches('\u{FEFF}');
194 serde_json::from_str(content)
195 .map_err(|e| format!("Failed to parse {}: {}", path.display(), e))
196 }
197
198 #[must_use]
200 pub fn all_dependency_names(&self) -> Vec<String> {
201 let mut deps = Vec::new();
202 if let Some(d) = &self.dependencies {
203 deps.extend(d.keys().cloned());
204 }
205 if let Some(d) = &self.dev_dependencies {
206 deps.extend(d.keys().cloned());
207 }
208 if let Some(d) = &self.peer_dependencies {
209 deps.extend(d.keys().cloned());
210 }
211 if let Some(d) = &self.optional_dependencies {
212 deps.extend(d.keys().cloned());
213 }
214 deps
215 }
216
217 #[must_use]
219 pub fn production_dependency_names(&self) -> Vec<String> {
220 self.dependencies
221 .as_ref()
222 .map(|d| d.keys().cloned().collect())
223 .unwrap_or_default()
224 }
225
226 #[must_use]
228 pub fn dev_dependency_names(&self) -> Vec<String> {
229 self.dev_dependencies
230 .as_ref()
231 .map(|d| d.keys().cloned().collect())
232 .unwrap_or_default()
233 }
234
235 #[must_use]
237 pub fn optional_dependency_names(&self) -> Vec<String> {
238 self.optional_dependencies
239 .as_ref()
240 .map(|d| d.keys().cloned().collect())
241 .unwrap_or_default()
242 }
243
244 #[must_use]
246 pub fn required_peer_dependency_names(&self) -> Vec<String> {
247 self.peer_dependencies
248 .as_ref()
249 .map(|deps| {
250 deps.keys()
251 .filter(|dep| !self.peer_dependency_is_optional(dep))
252 .cloned()
253 .collect()
254 })
255 .unwrap_or_default()
256 }
257
258 fn peer_dependency_is_optional(&self, dep: &str) -> bool {
259 self.peer_dependencies_meta
260 .as_ref()
261 .and_then(|meta| meta.get(dep))
262 .is_some_and(|meta| meta.optional)
263 }
264
265 #[must_use]
267 pub fn entry_points(&self) -> Vec<String> {
268 let mut entries = Vec::new();
269
270 if let Some(main) = &self.main {
271 entries.push(main.clone());
272 }
273 if let Some(module) = &self.module {
274 entries.push(module.clone());
275 }
276 if let Some(types) = &self.types {
277 entries.push(types.clone());
278 }
279 if let Some(typings) = &self.typings {
280 entries.push(typings.clone());
281 }
282 if let Some(source) = &self.source {
283 entries.push(source.clone());
284 }
285
286 if let Some(browser) = &self.browser {
287 match browser {
288 serde_json::Value::String(s) => entries.push(s.clone()),
289 serde_json::Value::Object(map) => {
290 for v in map.values() {
291 if let serde_json::Value::String(s) = v
292 && (s.starts_with("./") || s.starts_with("../"))
293 {
294 entries.push(s.clone());
295 }
296 }
297 }
298 _ => {}
299 }
300 }
301
302 if let Some(bin) = &self.bin {
303 match bin {
304 serde_json::Value::String(s) => entries.push(s.clone()),
305 serde_json::Value::Object(map) => {
306 for v in map.values() {
307 if let serde_json::Value::String(s) = v {
308 entries.push(s.clone());
309 }
310 }
311 }
312 _ => {}
313 }
314 }
315
316 if let Some(exports) = &self.exports {
317 extract_exports_entries(exports, &mut entries);
318 }
319
320 entries
321 }
322
323 #[must_use]
330 pub fn exports_subdirectories(&self) -> Vec<String> {
331 self.exports
332 .as_ref()
333 .map_or_else(Vec::new, extract_exports_subdirectories)
334 }
335
336 #[must_use]
338 pub fn workspace_patterns(&self) -> Vec<String> {
339 match &self.workspaces {
340 Some(serde_json::Value::Array(arr)) => arr
341 .iter()
342 .filter_map(|v| v.as_str().map(String::from))
343 .collect(),
344 Some(serde_json::Value::Object(obj)) => obj
345 .get("packages")
346 .and_then(|v| v.as_array())
347 .map(|arr| {
348 arr.iter()
349 .filter_map(|v| v.as_str().map(String::from))
350 .collect()
351 })
352 .unwrap_or_default(),
353 _ => Vec::new(),
354 }
355 }
356}
357
358fn extract_exports_subdirectories(exports: &serde_json::Value) -> Vec<String> {
365 let Some(map) = exports.as_object() else {
366 return Vec::new();
367 };
368
369 let skip_dirs = ["dist", "build", "out", "esm", "cjs", "lib", "node_modules"];
370 let mut dirs = rustc_hash::FxHashSet::default();
371
372 for key in map.keys() {
373 let stripped = key.strip_prefix("./").unwrap_or(key);
374 if let Some(first_segment) = stripped.split('/').next()
375 && !first_segment.is_empty()
376 && first_segment != "."
377 && first_segment != "package.json"
378 && !skip_dirs.contains(&first_segment)
379 {
380 dirs.insert(first_segment.to_owned());
381 }
382 }
383
384 dirs.into_iter().collect()
385}
386
387fn extract_exports_entries(value: &serde_json::Value, entries: &mut Vec<String>) {
389 match value {
390 serde_json::Value::String(s) if s.starts_with("./") || s.starts_with("../") => {
391 entries.push(s.clone());
392 }
393 serde_json::Value::Object(map) => {
394 for v in map.values() {
395 extract_exports_entries(v, entries);
396 }
397 }
398 serde_json::Value::Array(arr) => {
399 for v in arr {
400 extract_exports_entries(v, entries);
401 }
402 }
403 _ => {}
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 #[test]
412 fn package_json_workspace_patterns_array() {
413 let pkg: PackageJson =
414 serde_json::from_str(r#"{"workspaces": ["packages/*", "apps/*"]}"#).unwrap();
415 let patterns = pkg.workspace_patterns();
416 assert_eq!(patterns, vec!["packages/*", "apps/*"]);
417 }
418
419 #[test]
420 fn package_json_workspace_patterns_object() {
421 let pkg: PackageJson =
422 serde_json::from_str(r#"{"workspaces": {"packages": ["packages/*"]}}"#).unwrap();
423 let patterns = pkg.workspace_patterns();
424 assert_eq!(patterns, vec!["packages/*"]);
425 }
426
427 #[test]
428 fn package_json_workspace_patterns_none() {
429 let pkg: PackageJson = serde_json::from_str(r#"{"name": "test"}"#).unwrap();
430 let patterns = pkg.workspace_patterns();
431 assert!(patterns.is_empty());
432 }
433
434 #[test]
435 fn package_json_workspace_patterns_empty_array() {
436 let pkg: PackageJson = serde_json::from_str(r#"{"workspaces": []}"#).unwrap();
437 let patterns = pkg.workspace_patterns();
438 assert!(patterns.is_empty());
439 }
440
441 #[test]
442 fn package_json_load_valid() {
443 let temp_dir = std::env::temp_dir().join("fallow-test-pkg-json");
444 let _ = std::fs::create_dir_all(&temp_dir);
445 let pkg_path = temp_dir.join("package.json");
446 std::fs::write(&pkg_path, r#"{"name": "test", "main": "index.js"}"#).unwrap();
447
448 let pkg = PackageJson::load(&pkg_path).unwrap();
449 assert_eq!(pkg.name, Some("test".to_string()));
450 assert_eq!(pkg.main, Some("index.js".to_string()));
451
452 let _ = std::fs::remove_dir_all(&temp_dir);
453 }
454
455 #[test]
456 fn package_json_private_non_bool_values_are_ignored() {
457 for raw in [
458 r#"{"private": "true"}"#,
459 r#"{"private": 1}"#,
460 r#"{"private": null}"#,
461 ] {
462 let pkg: PackageJson = serde_json::from_str(raw).unwrap();
463 assert_eq!(pkg.private, None);
464 }
465
466 let pkg: PackageJson = serde_json::from_str(r#"{"private": true}"#).unwrap();
467 assert_eq!(pkg.private, Some(true));
468
469 let pkg: PackageJson = serde_json::from_str(r#"{"private": false}"#).unwrap();
470 assert_eq!(pkg.private, Some(false));
471 }
472
473 #[test]
474 fn package_json_files_preserves_string_array_values() {
475 let pkg: PackageJson =
476 serde_json::from_str(r#"{"files": ["index.js", "template-*", "dist"]}"#).unwrap();
477 assert_eq!(pkg.files, vec!["index.js", "template-*", "dist"]);
478 }
479
480 #[test]
481 fn package_json_files_missing_or_unexpected_shapes_are_ignored() {
482 let pkg: PackageJson = serde_json::from_str(r#"{"name": "pkg"}"#).unwrap();
483 assert!(pkg.files.is_empty());
484
485 let pkg: PackageJson = serde_json::from_str(r#"{"files": "dist"}"#).unwrap();
486 assert!(pkg.files.is_empty());
487
488 let pkg: PackageJson =
489 serde_json::from_str(r#"{"files": ["template-*", 42, false, null]}"#).unwrap();
490 assert_eq!(pkg.files, vec!["template-*"]);
491 }
492
493 #[test]
494 fn package_json_napi_config_preserves_current_string_fields() {
495 let pkg: PackageJson = serde_json::from_str(
496 r#"{
497 "napi": {
498 "binaryName": "srcmap-codec",
499 "packageName": "@srcmap/codec",
500 "targets": [
501 "aarch64-apple-darwin",
502 "x86_64-unknown-linux-gnu"
503 ]
504 }
505 }"#,
506 )
507 .unwrap();
508
509 let napi = pkg.napi.expect("napi config");
510 assert_eq!(napi.binary_name.as_deref(), Some("srcmap-codec"));
511 assert_eq!(napi.package_name.as_deref(), Some("@srcmap/codec"));
512 assert_eq!(
513 napi.targets,
514 vec![
515 "aarch64-apple-darwin".to_string(),
516 "x86_64-unknown-linux-gnu".to_string()
517 ]
518 );
519 }
520
521 #[test]
522 fn package_json_napi_config_ignores_unexpected_shapes() {
523 let pkg: PackageJson = serde_json::from_str(r#"{"napi": "enabled"}"#).unwrap();
524 assert!(pkg.napi.is_none());
525
526 let pkg: PackageJson = serde_json::from_str(
527 r#"{
528 "napi": {
529 "binaryName": 42,
530 "packageName": "",
531 "targets": ["x86_64-apple-darwin", false, null]
532 }
533 }"#,
534 )
535 .unwrap();
536
537 let napi = pkg.napi.expect("napi config");
538 assert!(napi.binary_name.is_none());
539 assert!(napi.package_name.is_none());
540 assert_eq!(napi.targets, vec!["x86_64-apple-darwin"]);
541 }
542
543 #[test]
544 fn package_json_load_missing_file() {
545 let result = PackageJson::load(std::path::Path::new("/nonexistent/package.json"));
546 assert!(result.is_err());
547 }
548
549 #[test]
550 fn package_json_entry_points_combined() {
551 let pkg: PackageJson = serde_json::from_str(
552 r#"{
553 "main": "dist/index.js",
554 "module": "dist/index.mjs",
555 "types": "dist/index.d.ts",
556 "typings": "dist/types.d.ts"
557 }"#,
558 )
559 .unwrap();
560 let entries = pkg.entry_points();
561 assert_eq!(entries.len(), 4);
562 assert!(entries.contains(&"dist/index.js".to_string()));
563 assert!(entries.contains(&"dist/index.mjs".to_string()));
564 assert!(entries.contains(&"dist/index.d.ts".to_string()));
565 assert!(entries.contains(&"dist/types.d.ts".to_string()));
566 }
567
568 #[test]
569 fn package_json_exports_nested() {
570 let pkg: PackageJson = serde_json::from_str(
571 r#"{
572 "exports": {
573 ".": {
574 "import": "./dist/index.mjs",
575 "require": "./dist/index.cjs"
576 },
577 "./utils": {
578 "import": "./dist/utils.mjs"
579 }
580 }
581 }"#,
582 )
583 .unwrap();
584 let entries = pkg.entry_points();
585 assert!(entries.contains(&"./dist/index.mjs".to_string()));
586 assert!(entries.contains(&"./dist/index.cjs".to_string()));
587 assert!(entries.contains(&"./dist/utils.mjs".to_string()));
588 }
589
590 #[test]
591 fn package_json_exports_array() {
592 let pkg: PackageJson = serde_json::from_str(
593 r#"{
594 "exports": {
595 ".": ["./dist/index.mjs", "./dist/index.cjs"]
596 }
597 }"#,
598 )
599 .unwrap();
600 let entries = pkg.entry_points();
601 assert!(entries.contains(&"./dist/index.mjs".to_string()));
602 assert!(entries.contains(&"./dist/index.cjs".to_string()));
603 }
604
605 #[test]
606 fn extract_exports_ignores_non_relative() {
607 let pkg: PackageJson = serde_json::from_str(
608 r#"{
609 "exports": {
610 ".": "not-a-relative-path"
611 }
612 }"#,
613 )
614 .unwrap();
615 let entries = pkg.entry_points();
616 assert!(entries.is_empty());
617 }
618
619 #[test]
620 fn package_json_source_field() {
621 let pkg: PackageJson = serde_json::from_str(
622 r#"{
623 "main": "dist/index.js",
624 "source": "src/index.ts"
625 }"#,
626 )
627 .unwrap();
628 let entries = pkg.entry_points();
629 assert!(entries.contains(&"src/index.ts".to_string()));
630 assert!(entries.contains(&"dist/index.js".to_string()));
631 }
632
633 #[test]
634 fn package_json_browser_field_string() {
635 let pkg: PackageJson = serde_json::from_str(
636 r#"{
637 "browser": "./dist/browser.js"
638 }"#,
639 )
640 .unwrap();
641 let entries = pkg.entry_points();
642 assert!(entries.contains(&"./dist/browser.js".to_string()));
643 }
644
645 #[test]
646 fn package_json_browser_field_object() {
647 let pkg: PackageJson = serde_json::from_str(
648 r#"{
649 "browser": {
650 "./server.js": "./browser.js",
651 "module-name": false
652 }
653 }"#,
654 )
655 .unwrap();
656 let entries = pkg.entry_points();
657 assert!(entries.contains(&"./browser.js".to_string()));
658 assert_eq!(entries.len(), 1);
659 }
660
661 #[test]
662 fn package_json_exports_string() {
663 let pkg: PackageJson = serde_json::from_str(r#"{"exports": "./dist/index.js"}"#).unwrap();
664 let entries = pkg.entry_points();
665 assert_eq!(entries, vec!["./dist/index.js"]);
666 }
667
668 #[test]
669 fn package_json_workspace_patterns_object_with_nohoist() {
670 let pkg: PackageJson = serde_json::from_str(
671 r#"{
672 "workspaces": {
673 "packages": ["packages/*", "apps/*"],
674 "nohoist": ["**/react-native"]
675 }
676 }"#,
677 )
678 .unwrap();
679 let patterns = pkg.workspace_patterns();
680 assert_eq!(patterns, vec!["packages/*", "apps/*"]);
681 }
682
683 #[test]
684 fn package_json_missing_optional_fields() {
685 let pkg: PackageJson = serde_json::from_str(r"{}").unwrap();
686 assert!(pkg.name.is_none());
687 assert!(pkg.main.is_none());
688 assert!(pkg.module.is_none());
689 assert!(pkg.types.is_none());
690 assert!(pkg.typings.is_none());
691 assert!(pkg.source.is_none());
692 assert!(pkg.browser.is_none());
693 assert!(pkg.bin.is_none());
694 assert!(pkg.exports.is_none());
695 assert!(pkg.dependencies.is_none());
696 assert!(pkg.dev_dependencies.is_none());
697 assert!(pkg.peer_dependencies.is_none());
698 assert!(pkg.peer_dependencies_meta.is_none());
699 assert!(pkg.optional_dependencies.is_none());
700 assert!(pkg.scripts.is_none());
701 assert!(pkg.workspaces.is_none());
702 assert!(pkg.entry_points().is_empty());
703 assert!(pkg.workspace_patterns().is_empty());
704 assert!(pkg.all_dependency_names().is_empty());
705 }
706
707 #[test]
708 fn package_json_all_dependency_names() {
709 let pkg: PackageJson = serde_json::from_str(
710 r#"{
711 "dependencies": {"react": "^18", "react-dom": "^18"},
712 "devDependencies": {"typescript": "^5"},
713 "peerDependencies": {"node": ">=18"},
714 "optionalDependencies": {"fsevents": "^2"}
715 }"#,
716 )
717 .unwrap();
718 let deps = pkg.all_dependency_names();
719 assert_eq!(deps.len(), 5);
720 assert!(deps.contains(&"react".to_string()));
721 assert!(deps.contains(&"react-dom".to_string()));
722 assert!(deps.contains(&"typescript".to_string()));
723 assert!(deps.contains(&"node".to_string()));
724 assert!(deps.contains(&"fsevents".to_string()));
725 }
726
727 #[test]
728 fn package_json_production_dependency_names() {
729 let pkg: PackageJson = serde_json::from_str(
730 r#"{
731 "dependencies": {"react": "^18"},
732 "devDependencies": {"typescript": "^5"}
733 }"#,
734 )
735 .unwrap();
736 let prod = pkg.production_dependency_names();
737 assert_eq!(prod, vec!["react"]);
738 let dev = pkg.dev_dependency_names();
739 assert_eq!(dev, vec!["typescript"]);
740 }
741
742 #[test]
743 fn package_json_bin_field_string() {
744 let pkg: PackageJson = serde_json::from_str(r#"{"bin": "./cli.js"}"#).unwrap();
745 let entries = pkg.entry_points();
746 assert!(entries.contains(&"./cli.js".to_string()));
747 }
748
749 #[test]
750 fn package_json_bin_field_object() {
751 let pkg: PackageJson = serde_json::from_str(
752 r#"{"bin": {"my-cli": "./bin/cli.js", "my-tool": "./bin/tool.js"}}"#,
753 )
754 .unwrap();
755 let entries = pkg.entry_points();
756 assert!(entries.contains(&"./bin/cli.js".to_string()));
757 assert!(entries.contains(&"./bin/tool.js".to_string()));
758 }
759
760 #[test]
761 fn package_json_exports_deeply_nested() {
762 let pkg: PackageJson = serde_json::from_str(
763 r#"{
764 "exports": {
765 ".": {
766 "node": {
767 "import": "./dist/node.mjs",
768 "require": "./dist/node.cjs"
769 },
770 "browser": {
771 "import": "./dist/browser.mjs"
772 }
773 }
774 }
775 }"#,
776 )
777 .unwrap();
778 let entries = pkg.entry_points();
779 assert_eq!(entries.len(), 3);
780 assert!(entries.contains(&"./dist/node.mjs".to_string()));
781 assert!(entries.contains(&"./dist/node.cjs".to_string()));
782 assert!(entries.contains(&"./dist/browser.mjs".to_string()));
783 }
784
785 #[test]
786 fn package_json_peer_deps_only() {
787 let pkg: PackageJson =
788 serde_json::from_str(r#"{"peerDependencies": {"react": "^18", "react-dom": "^18"}}"#)
789 .unwrap();
790 let all = pkg.all_dependency_names();
791 assert_eq!(all.len(), 2);
792 assert!(all.contains(&"react".to_string()));
793 assert!(all.contains(&"react-dom".to_string()));
794
795 assert!(pkg.production_dependency_names().is_empty());
796 assert!(pkg.dev_dependency_names().is_empty());
797 }
798
799 #[test]
800 fn package_json_required_peer_dependency_names_excludes_optional_peers() {
801 let pkg: PackageJson = serde_json::from_str(
802 r#"{
803 "peerDependencies": {"react": "^18", "typescript": "^5"},
804 "peerDependenciesMeta": {"typescript": {"optional": true}}
805 }"#,
806 )
807 .unwrap();
808 assert_eq!(pkg.required_peer_dependency_names(), vec!["react"]);
809 }
810
811 #[test]
812 fn package_json_optional_deps_in_all_names() {
813 let pkg: PackageJson =
814 serde_json::from_str(r#"{"optionalDependencies": {"fsevents": "^2"}}"#).unwrap();
815 let all = pkg.all_dependency_names();
816 assert!(all.contains(&"fsevents".to_string()));
817 }
818
819 #[test]
820 fn package_json_browser_array_ignored() {
821 let pkg: PackageJson =
822 serde_json::from_str(r#"{"browser": ["./a.js", "./b.js"]}"#).unwrap();
823 let entries = pkg.entry_points();
824 assert!(
825 entries.is_empty(),
826 "array browser field should not produce entries"
827 );
828 }
829
830 #[test]
831 fn package_json_browser_object_non_relative_skipped() {
832 let pkg: PackageJson = serde_json::from_str(
833 r#"{"browser": {"crypto": false, "./local.js": "./browser-local.js"}}"#,
834 )
835 .unwrap();
836 let entries = pkg.entry_points();
837 assert_eq!(entries.len(), 1);
838 assert!(entries.contains(&"./browser-local.js".to_string()));
839 }
840
841 #[test]
842 fn package_json_exports_null_value() {
843 let pkg: PackageJson =
844 serde_json::from_str(r#"{"exports": {".": "./dist/index.js", "./internal": null}}"#)
845 .unwrap();
846 let entries = pkg.entry_points();
847 assert_eq!(entries.len(), 1);
848 assert!(entries.contains(&"./dist/index.js".to_string()));
849 }
850
851 #[test]
852 fn package_json_exports_empty_object() {
853 let pkg: PackageJson = serde_json::from_str(r#"{"exports": {}}"#).unwrap();
854 let entries = pkg.entry_points();
855 assert!(entries.is_empty());
856 }
857
858 #[test]
859 fn package_json_workspace_patterns_string_value_ignored() {
860 let pkg: PackageJson = serde_json::from_str(r#"{"workspaces": "packages/*"}"#).unwrap();
861 let patterns = pkg.workspace_patterns();
862 assert!(patterns.is_empty());
863 }
864
865 #[test]
866 fn package_json_workspace_patterns_object_missing_packages() {
867 let pkg: PackageJson =
868 serde_json::from_str(r#"{"workspaces": {"nohoist": ["**/react-native"]}}"#).unwrap();
869 let patterns = pkg.workspace_patterns();
870 assert!(patterns.is_empty());
871 }
872
873 #[test]
874 fn package_json_load_invalid_json() {
875 let temp_dir = std::env::temp_dir().join("fallow-test-invalid-pkg-json");
876 let _ = std::fs::create_dir_all(&temp_dir);
877 let pkg_path = temp_dir.join("package.json");
878 std::fs::write(&pkg_path, "{ not valid json }").unwrap();
879
880 let result = PackageJson::load(&pkg_path);
881 assert!(result.is_err());
882 let err = result.unwrap_err();
883 assert!(err.contains("Failed to parse"), "got: {err}");
884
885 let _ = std::fs::remove_dir_all(&temp_dir);
886 }
887
888 #[test]
889 fn package_json_bin_object_non_string_values_skipped() {
890 let pkg: PackageJson =
891 serde_json::from_str(r#"{"bin": {"cli": "./bin/cli.js", "bad": 42}}"#).unwrap();
892 let entries = pkg.entry_points();
893 assert_eq!(entries.len(), 1);
894 assert!(entries.contains(&"./bin/cli.js".to_string()));
895 }
896
897 #[test]
898 fn package_json_default() {
899 let pkg = PackageJson::default();
900 assert!(pkg.name.is_none());
901 assert!(pkg.main.is_none());
902 assert!(pkg.entry_points().is_empty());
903 assert!(pkg.all_dependency_names().is_empty());
904 assert!(pkg.workspace_patterns().is_empty());
905 }
906
907 #[test]
908 fn exports_subdirectories_preact_style() {
909 let pkg: PackageJson = serde_json::from_str(
910 r#"{
911 "exports": {
912 ".": "./dist/index.js",
913 "./compat": { "import": "./compat/dist/compat.mjs" },
914 "./hooks": { "import": "./hooks/dist/hooks.mjs" },
915 "./debug": { "import": "./debug/dist/debug.mjs" },
916 "./jsx-runtime": { "import": "./jsx-runtime/dist/jsx.mjs" },
917 "./package.json": "./package.json"
918 }
919 }"#,
920 )
921 .unwrap();
922 let mut dirs = pkg.exports_subdirectories();
923 dirs.sort();
924 assert_eq!(dirs, vec!["compat", "debug", "hooks", "jsx-runtime"]);
925 }
926
927 #[test]
928 fn exports_subdirectories_skips_dist_dirs() {
929 let pkg: PackageJson = serde_json::from_str(
930 r#"{
931 "exports": {
932 "./dist/index.js": "./dist/index.js",
933 "./build/bundle.js": "./build/bundle.js",
934 "./lib/utils": "./lib/utils.js",
935 "./compat": "./compat/index.js"
936 }
937 }"#,
938 )
939 .unwrap();
940 let dirs = pkg.exports_subdirectories();
941 assert_eq!(dirs, vec!["compat"]);
942 }
943
944 #[test]
945 fn exports_subdirectories_no_exports() {
946 let pkg: PackageJson = serde_json::from_str(r#"{"main": "index.js"}"#).unwrap();
947 assert!(pkg.exports_subdirectories().is_empty());
948 }
949
950 #[test]
951 fn exports_subdirectories_dot_only() {
952 let pkg: PackageJson =
953 serde_json::from_str(r#"{"exports": {".": "./dist/index.js"}}"#).unwrap();
954 assert!(pkg.exports_subdirectories().is_empty());
955 }
956}