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