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