1use serde::{Deserialize, Serialize};
2
3#[expect(clippy::disallowed_types)]
7type StdHashMap<K, V> = std::collections::HashMap<K, V>;
8
9#[derive(Debug, Clone, Default, Deserialize, Serialize)]
11pub struct PackageJson {
12 #[serde(default)]
13 pub name: Option<String>,
14 #[serde(default)]
15 pub main: Option<String>,
16 #[serde(default)]
17 pub module: Option<String>,
18 #[serde(default)]
19 pub types: Option<String>,
20 #[serde(default)]
21 pub typings: Option<String>,
22 #[serde(default)]
23 pub source: Option<String>,
24 #[serde(default)]
25 pub browser: Option<serde_json::Value>,
26 #[serde(default)]
27 pub bin: Option<serde_json::Value>,
28 #[serde(default)]
29 pub exports: Option<serde_json::Value>,
30 #[serde(default)]
31 pub dependencies: Option<StdHashMap<String, String>>,
32 #[serde(default, rename = "devDependencies")]
33 pub dev_dependencies: Option<StdHashMap<String, String>>,
34 #[serde(default, rename = "peerDependencies")]
35 pub peer_dependencies: Option<StdHashMap<String, String>>,
36 #[serde(default, rename = "optionalDependencies")]
37 pub optional_dependencies: Option<StdHashMap<String, String>>,
38 #[serde(default)]
39 pub scripts: Option<StdHashMap<String, String>>,
40 #[serde(default)]
41 pub workspaces: Option<serde_json::Value>,
42}
43
44impl PackageJson {
45 pub fn load(path: &std::path::Path) -> Result<Self, String> {
51 let content = std::fs::read_to_string(path)
52 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
53 serde_json::from_str(&content)
54 .map_err(|e| format!("Failed to parse {}: {}", path.display(), e))
55 }
56
57 #[must_use]
59 pub fn all_dependency_names(&self) -> Vec<String> {
60 let mut deps = Vec::new();
61 if let Some(d) = &self.dependencies {
62 deps.extend(d.keys().cloned());
63 }
64 if let Some(d) = &self.dev_dependencies {
65 deps.extend(d.keys().cloned());
66 }
67 if let Some(d) = &self.peer_dependencies {
68 deps.extend(d.keys().cloned());
69 }
70 if let Some(d) = &self.optional_dependencies {
71 deps.extend(d.keys().cloned());
72 }
73 deps
74 }
75
76 #[must_use]
78 pub fn production_dependency_names(&self) -> Vec<String> {
79 self.dependencies
80 .as_ref()
81 .map(|d| d.keys().cloned().collect())
82 .unwrap_or_default()
83 }
84
85 #[must_use]
87 pub fn dev_dependency_names(&self) -> Vec<String> {
88 self.dev_dependencies
89 .as_ref()
90 .map(|d| d.keys().cloned().collect())
91 .unwrap_or_default()
92 }
93
94 #[must_use]
96 pub fn optional_dependency_names(&self) -> Vec<String> {
97 self.optional_dependencies
98 .as_ref()
99 .map(|d| d.keys().cloned().collect())
100 .unwrap_or_default()
101 }
102
103 #[must_use]
105 pub fn entry_points(&self) -> Vec<String> {
106 let mut entries = Vec::new();
107
108 if let Some(main) = &self.main {
109 entries.push(main.clone());
110 }
111 if let Some(module) = &self.module {
112 entries.push(module.clone());
113 }
114 if let Some(types) = &self.types {
115 entries.push(types.clone());
116 }
117 if let Some(typings) = &self.typings {
118 entries.push(typings.clone());
119 }
120 if let Some(source) = &self.source {
121 entries.push(source.clone());
122 }
123
124 if let Some(browser) = &self.browser {
126 match browser {
127 serde_json::Value::String(s) => entries.push(s.clone()),
128 serde_json::Value::Object(map) => {
129 for v in map.values() {
130 if let serde_json::Value::String(s) = v
131 && (s.starts_with("./") || s.starts_with("../"))
132 {
133 entries.push(s.clone());
134 }
135 }
136 }
137 _ => {}
138 }
139 }
140
141 if let Some(bin) = &self.bin {
143 match bin {
144 serde_json::Value::String(s) => entries.push(s.clone()),
145 serde_json::Value::Object(map) => {
146 for v in map.values() {
147 if let serde_json::Value::String(s) = v {
148 entries.push(s.clone());
149 }
150 }
151 }
152 _ => {}
153 }
154 }
155
156 if let Some(exports) = &self.exports {
158 extract_exports_entries(exports, &mut entries);
159 }
160
161 entries
162 }
163
164 #[must_use]
166 pub fn workspace_patterns(&self) -> Vec<String> {
167 match &self.workspaces {
168 Some(serde_json::Value::Array(arr)) => arr
169 .iter()
170 .filter_map(|v| v.as_str().map(String::from))
171 .collect(),
172 Some(serde_json::Value::Object(obj)) => obj
173 .get("packages")
174 .and_then(|v| v.as_array())
175 .map(|arr| {
176 arr.iter()
177 .filter_map(|v| v.as_str().map(String::from))
178 .collect()
179 })
180 .unwrap_or_default(),
181 _ => Vec::new(),
182 }
183 }
184}
185
186fn extract_exports_entries(value: &serde_json::Value, entries: &mut Vec<String>) {
188 match value {
189 serde_json::Value::String(s) => {
190 if s.starts_with("./") || s.starts_with("../") {
191 entries.push(s.clone());
192 }
193 }
194 serde_json::Value::Object(map) => {
195 for v in map.values() {
196 extract_exports_entries(v, entries);
197 }
198 }
199 serde_json::Value::Array(arr) => {
200 for v in arr {
201 extract_exports_entries(v, entries);
202 }
203 }
204 _ => {}
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn package_json_workspace_patterns_array() {
214 let pkg: PackageJson =
215 serde_json::from_str(r#"{"workspaces": ["packages/*", "apps/*"]}"#).unwrap();
216 let patterns = pkg.workspace_patterns();
217 assert_eq!(patterns, vec!["packages/*", "apps/*"]);
218 }
219
220 #[test]
221 fn package_json_workspace_patterns_object() {
222 let pkg: PackageJson =
223 serde_json::from_str(r#"{"workspaces": {"packages": ["packages/*"]}}"#).unwrap();
224 let patterns = pkg.workspace_patterns();
225 assert_eq!(patterns, vec!["packages/*"]);
226 }
227
228 #[test]
229 fn package_json_workspace_patterns_none() {
230 let pkg: PackageJson = serde_json::from_str(r#"{"name": "test"}"#).unwrap();
231 let patterns = pkg.workspace_patterns();
232 assert!(patterns.is_empty());
233 }
234
235 #[test]
236 fn package_json_workspace_patterns_empty_array() {
237 let pkg: PackageJson = serde_json::from_str(r#"{"workspaces": []}"#).unwrap();
238 let patterns = pkg.workspace_patterns();
239 assert!(patterns.is_empty());
240 }
241
242 #[test]
243 fn package_json_load_valid() {
244 let temp_dir = std::env::temp_dir().join("fallow-test-pkg-json");
245 let _ = std::fs::create_dir_all(&temp_dir);
246 let pkg_path = temp_dir.join("package.json");
247 std::fs::write(&pkg_path, r#"{"name": "test", "main": "index.js"}"#).unwrap();
248
249 let pkg = PackageJson::load(&pkg_path).unwrap();
250 assert_eq!(pkg.name, Some("test".to_string()));
251 assert_eq!(pkg.main, Some("index.js".to_string()));
252
253 let _ = std::fs::remove_dir_all(&temp_dir);
254 }
255
256 #[test]
257 fn package_json_load_missing_file() {
258 let result = PackageJson::load(std::path::Path::new("/nonexistent/package.json"));
259 assert!(result.is_err());
260 }
261
262 #[test]
263 fn package_json_entry_points_combined() {
264 let pkg: PackageJson = serde_json::from_str(
265 r#"{
266 "main": "dist/index.js",
267 "module": "dist/index.mjs",
268 "types": "dist/index.d.ts",
269 "typings": "dist/types.d.ts"
270 }"#,
271 )
272 .unwrap();
273 let entries = pkg.entry_points();
274 assert_eq!(entries.len(), 4);
275 assert!(entries.contains(&"dist/index.js".to_string()));
276 assert!(entries.contains(&"dist/index.mjs".to_string()));
277 assert!(entries.contains(&"dist/index.d.ts".to_string()));
278 assert!(entries.contains(&"dist/types.d.ts".to_string()));
279 }
280
281 #[test]
282 fn package_json_exports_nested() {
283 let pkg: PackageJson = serde_json::from_str(
284 r#"{
285 "exports": {
286 ".": {
287 "import": "./dist/index.mjs",
288 "require": "./dist/index.cjs"
289 },
290 "./utils": {
291 "import": "./dist/utils.mjs"
292 }
293 }
294 }"#,
295 )
296 .unwrap();
297 let entries = pkg.entry_points();
298 assert!(entries.contains(&"./dist/index.mjs".to_string()));
299 assert!(entries.contains(&"./dist/index.cjs".to_string()));
300 assert!(entries.contains(&"./dist/utils.mjs".to_string()));
301 }
302
303 #[test]
304 fn package_json_exports_array() {
305 let pkg: PackageJson = serde_json::from_str(
306 r#"{
307 "exports": {
308 ".": ["./dist/index.mjs", "./dist/index.cjs"]
309 }
310 }"#,
311 )
312 .unwrap();
313 let entries = pkg.entry_points();
314 assert!(entries.contains(&"./dist/index.mjs".to_string()));
315 assert!(entries.contains(&"./dist/index.cjs".to_string()));
316 }
317
318 #[test]
319 fn extract_exports_ignores_non_relative() {
320 let pkg: PackageJson = serde_json::from_str(
321 r#"{
322 "exports": {
323 ".": "not-a-relative-path"
324 }
325 }"#,
326 )
327 .unwrap();
328 let entries = pkg.entry_points();
329 assert!(entries.is_empty());
331 }
332
333 #[test]
334 fn package_json_source_field() {
335 let pkg: PackageJson = serde_json::from_str(
336 r#"{
337 "main": "dist/index.js",
338 "source": "src/index.ts"
339 }"#,
340 )
341 .unwrap();
342 let entries = pkg.entry_points();
343 assert!(entries.contains(&"src/index.ts".to_string()));
344 assert!(entries.contains(&"dist/index.js".to_string()));
345 }
346
347 #[test]
348 fn package_json_browser_field_string() {
349 let pkg: PackageJson = serde_json::from_str(
350 r#"{
351 "browser": "./dist/browser.js"
352 }"#,
353 )
354 .unwrap();
355 let entries = pkg.entry_points();
356 assert!(entries.contains(&"./dist/browser.js".to_string()));
357 }
358
359 #[test]
360 fn package_json_browser_field_object() {
361 let pkg: PackageJson = serde_json::from_str(
362 r#"{
363 "browser": {
364 "./server.js": "./browser.js",
365 "module-name": false
366 }
367 }"#,
368 )
369 .unwrap();
370 let entries = pkg.entry_points();
371 assert!(entries.contains(&"./browser.js".to_string()));
372 assert_eq!(entries.len(), 1);
374 }
375
376 #[test]
377 fn package_json_exports_string() {
378 let pkg: PackageJson = serde_json::from_str(r#"{"exports": "./dist/index.js"}"#).unwrap();
379 let entries = pkg.entry_points();
380 assert_eq!(entries, vec!["./dist/index.js"]);
381 }
382
383 #[test]
384 fn package_json_workspace_patterns_object_with_nohoist() {
385 let pkg: PackageJson = serde_json::from_str(
386 r#"{
387 "workspaces": {
388 "packages": ["packages/*", "apps/*"],
389 "nohoist": ["**/react-native"]
390 }
391 }"#,
392 )
393 .unwrap();
394 let patterns = pkg.workspace_patterns();
395 assert_eq!(patterns, vec!["packages/*", "apps/*"]);
396 }
397
398 #[test]
399 fn package_json_missing_optional_fields() {
400 let pkg: PackageJson = serde_json::from_str(r"{}").unwrap();
401 assert!(pkg.name.is_none());
402 assert!(pkg.main.is_none());
403 assert!(pkg.module.is_none());
404 assert!(pkg.types.is_none());
405 assert!(pkg.typings.is_none());
406 assert!(pkg.source.is_none());
407 assert!(pkg.browser.is_none());
408 assert!(pkg.bin.is_none());
409 assert!(pkg.exports.is_none());
410 assert!(pkg.dependencies.is_none());
411 assert!(pkg.dev_dependencies.is_none());
412 assert!(pkg.peer_dependencies.is_none());
413 assert!(pkg.optional_dependencies.is_none());
414 assert!(pkg.scripts.is_none());
415 assert!(pkg.workspaces.is_none());
416 assert!(pkg.entry_points().is_empty());
417 assert!(pkg.workspace_patterns().is_empty());
418 assert!(pkg.all_dependency_names().is_empty());
419 }
420
421 #[test]
422 fn package_json_all_dependency_names() {
423 let pkg: PackageJson = serde_json::from_str(
424 r#"{
425 "dependencies": {"react": "^18", "react-dom": "^18"},
426 "devDependencies": {"typescript": "^5"},
427 "peerDependencies": {"node": ">=18"},
428 "optionalDependencies": {"fsevents": "^2"}
429 }"#,
430 )
431 .unwrap();
432 let deps = pkg.all_dependency_names();
433 assert_eq!(deps.len(), 5);
434 assert!(deps.contains(&"react".to_string()));
435 assert!(deps.contains(&"react-dom".to_string()));
436 assert!(deps.contains(&"typescript".to_string()));
437 assert!(deps.contains(&"node".to_string()));
438 assert!(deps.contains(&"fsevents".to_string()));
439 }
440
441 #[test]
442 fn package_json_production_dependency_names() {
443 let pkg: PackageJson = serde_json::from_str(
444 r#"{
445 "dependencies": {"react": "^18"},
446 "devDependencies": {"typescript": "^5"}
447 }"#,
448 )
449 .unwrap();
450 let prod = pkg.production_dependency_names();
451 assert_eq!(prod, vec!["react"]);
452 let dev = pkg.dev_dependency_names();
453 assert_eq!(dev, vec!["typescript"]);
454 }
455
456 #[test]
457 fn package_json_bin_field_string() {
458 let pkg: PackageJson = serde_json::from_str(r#"{"bin": "./cli.js"}"#).unwrap();
459 let entries = pkg.entry_points();
460 assert!(entries.contains(&"./cli.js".to_string()));
461 }
462
463 #[test]
464 fn package_json_bin_field_object() {
465 let pkg: PackageJson = serde_json::from_str(
466 r#"{"bin": {"my-cli": "./bin/cli.js", "my-tool": "./bin/tool.js"}}"#,
467 )
468 .unwrap();
469 let entries = pkg.entry_points();
470 assert!(entries.contains(&"./bin/cli.js".to_string()));
471 assert!(entries.contains(&"./bin/tool.js".to_string()));
472 }
473
474 #[test]
475 fn package_json_exports_deeply_nested() {
476 let pkg: PackageJson = serde_json::from_str(
477 r#"{
478 "exports": {
479 ".": {
480 "node": {
481 "import": "./dist/node.mjs",
482 "require": "./dist/node.cjs"
483 },
484 "browser": {
485 "import": "./dist/browser.mjs"
486 }
487 }
488 }
489 }"#,
490 )
491 .unwrap();
492 let entries = pkg.entry_points();
493 assert_eq!(entries.len(), 3);
494 assert!(entries.contains(&"./dist/node.mjs".to_string()));
495 assert!(entries.contains(&"./dist/node.cjs".to_string()));
496 assert!(entries.contains(&"./dist/browser.mjs".to_string()));
497 }
498}