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