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