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