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