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