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