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