1use std::path::{Path, PathBuf};
2use tokio::fs;
3
4use super::detect::PackageManager;
5
6pub async fn detect_package_manager(start_path: &Path) -> PackageManager {
9 for name in &["pnpm-lock.yaml", "pnpm-lock.yml", "pnpm-workspace.yaml"] {
10 if fs::metadata(start_path.join(name)).await.is_ok() {
11 return PackageManager::Pnpm;
12 }
13 }
14 PackageManager::Npm
15}
16
17#[derive(Debug, Clone)]
19pub enum WorkspaceType {
20 Npm,
21 Pnpm,
22 None,
23}
24
25#[derive(Debug, Clone)]
27pub struct WorkspaceConfig {
28 pub ws_type: WorkspaceType,
29 pub patterns: Vec<String>,
30}
31
32#[derive(Debug, Clone)]
34pub struct PackageJsonLocation {
35 pub path: PathBuf,
36 pub is_root: bool,
37 pub is_workspace: bool,
38 pub workspace_pattern: Option<String>,
39}
40
41#[derive(Debug)]
43pub struct PackageJsonFindResult {
44 pub files: Vec<PackageJsonLocation>,
45 pub workspace_type: WorkspaceType,
46}
47
48pub async fn find_package_json_files(start_path: &Path) -> PackageJsonFindResult {
50 let mut results = Vec::new();
51 let root_package_json = start_path.join("package.json");
52
53 let mut root_exists = false;
54 let mut workspace_config = WorkspaceConfig {
55 ws_type: WorkspaceType::None,
56 patterns: Vec::new(),
57 };
58
59 if fs::metadata(&root_package_json).await.is_ok() {
60 root_exists = true;
61 workspace_config = detect_workspaces(&root_package_json).await;
62 results.push(PackageJsonLocation {
63 path: root_package_json,
64 is_root: true,
65 is_workspace: false,
66 workspace_pattern: None,
67 });
68 }
69
70 match workspace_config.ws_type {
71 WorkspaceType::None => {
72 if root_exists {
73 let nested = find_nested_package_json_files(start_path).await;
74 results.extend(nested);
75 }
76 }
77 _ => {
78 let ws_packages = find_workspace_packages(start_path, &workspace_config).await;
79 results.extend(ws_packages);
80 }
81 }
82
83 let mut seen = std::collections::HashSet::new();
88 results.retain(|loc| seen.insert(loc.path.clone()));
89
90 PackageJsonFindResult {
91 files: results,
92 workspace_type: workspace_config.ws_type,
93 }
94}
95
96pub async fn detect_workspaces(package_json_path: &Path) -> WorkspaceConfig {
98 let default = WorkspaceConfig {
99 ws_type: WorkspaceType::None,
100 patterns: Vec::new(),
101 };
102
103 let dir = package_json_path.parent().unwrap_or(Path::new("."));
111 let pnpm_workspace = dir.join("pnpm-workspace.yaml");
112 if let Ok(yaml_content) = fs::read_to_string(&pnpm_workspace).await {
113 let patterns = parse_pnpm_workspace_patterns(&yaml_content);
114 return WorkspaceConfig {
115 ws_type: WorkspaceType::Pnpm,
116 patterns,
117 };
118 }
119
120 let content = match fs::read_to_string(package_json_path).await {
121 Ok(c) => c,
122 Err(_) => return default,
123 };
124
125 let pkg: serde_json::Value = match serde_json::from_str(&content) {
126 Ok(v) => v,
127 Err(_) => return default,
128 };
129
130 if let Some(workspaces) = pkg.get("workspaces") {
132 let patterns = if let Some(arr) = workspaces.as_array() {
133 arr.iter()
134 .filter_map(|v| v.as_str().map(String::from))
135 .collect()
136 } else if let Some(obj) = workspaces.as_object() {
137 obj.get("packages")
138 .and_then(|v| v.as_array())
139 .map(|arr| {
140 arr.iter()
141 .filter_map(|v| v.as_str().map(String::from))
142 .collect()
143 })
144 .unwrap_or_default()
145 } else {
146 Vec::new()
147 };
148
149 return WorkspaceConfig {
150 ws_type: WorkspaceType::Npm,
151 patterns,
152 };
153 }
154
155 default
156}
157
158fn parse_pnpm_workspace_patterns(yaml_content: &str) -> Vec<String> {
160 let mut patterns = Vec::new();
161 let mut in_packages = false;
162
163 for line in yaml_content.lines() {
164 let trimmed = line.trim();
165
166 if trimmed == "packages:" {
167 in_packages = true;
168 continue;
169 }
170
171 if in_packages {
172 if !trimmed.is_empty() && !trimmed.starts_with('-') && !trimmed.starts_with('#') {
173 break;
174 }
175
176 if let Some(rest) = trimmed.strip_prefix('-') {
177 let item = parse_yaml_list_value(rest);
178 if !item.is_empty() {
179 patterns.push(item);
180 }
181 }
182 }
183 }
184
185 patterns
186}
187
188fn parse_yaml_list_value(raw: &str) -> String {
191 let s = raw.trim();
192
193 for q in ['\'', '"'] {
197 if let Some(rest) = s.strip_prefix(q) {
198 if let Some(end) = rest.find(q) {
199 return rest[..end].to_string();
200 }
201 }
202 }
203
204 let bytes = s.as_bytes();
206 let comment_start =
207 (1..bytes.len()).find(|&i| bytes[i] == b'#' && bytes[i - 1].is_ascii_whitespace());
208 let value = match comment_start {
209 Some(idx) => &s[..idx],
210 None => s,
211 };
212 value.trim().to_string()
213}
214
215async fn find_workspace_packages(
217 root_path: &Path,
218 config: &WorkspaceConfig,
219) -> Vec<PackageJsonLocation> {
220 let mut results = Vec::new();
221
222 for pattern in &config.patterns {
223 let packages = find_packages_matching_pattern(root_path, pattern).await;
224 for p in packages {
225 results.push(PackageJsonLocation {
226 path: p,
227 is_root: false,
228 is_workspace: true,
229 workspace_pattern: Some(pattern.clone()),
230 });
231 }
232 }
233
234 results
235}
236
237async fn find_packages_matching_pattern(root_path: &Path, pattern: &str) -> Vec<PathBuf> {
239 let mut results = Vec::new();
240
241 let (prefix, last) = pattern.rsplit_once('/').unwrap_or(("", pattern));
246
247 match last {
248 "*" | "**" => {
249 let search_path = if prefix.is_empty() {
250 root_path.to_path_buf()
251 } else {
252 root_path.join(prefix)
253 };
254 if last == "*" {
255 search_one_level(&search_path, &mut results).await;
256 } else {
257 search_recursive(&search_path, &mut results).await;
258 }
259 }
260 _ => {
261 let pkg_json = root_path.join(pattern).join("package.json");
262 if fs::metadata(&pkg_json).await.is_ok() {
263 results.push(pkg_json);
264 }
265 }
266 }
267
268 results
269}
270
271fn is_ignored_dir(name: &str) -> bool {
274 name.starts_with('.') || name == "node_modules" || name == "dist" || name == "build"
275}
276
277async fn search_one_level(dir: &Path, results: &mut Vec<PathBuf>) {
279 let mut entries = match fs::read_dir(dir).await {
280 Ok(e) => e,
281 Err(_) => return,
282 };
283
284 while let Ok(Some(entry)) = entries.next_entry().await {
285 let ft = match entry.file_type().await {
286 Ok(ft) => ft,
287 Err(_) => continue,
288 };
289 if !ft.is_dir() {
290 continue;
291 }
292 if is_ignored_dir(&entry.file_name().to_string_lossy()) {
295 continue;
296 }
297 let pkg_json = entry.path().join("package.json");
298 if fs::metadata(&pkg_json).await.is_ok() {
299 results.push(pkg_json);
300 }
301 }
302}
303
304async fn search_recursive(dir: &Path, results: &mut Vec<PathBuf>) {
306 let mut entries = match fs::read_dir(dir).await {
307 Ok(e) => e,
308 Err(_) => return,
309 };
310
311 while let Ok(Some(entry)) = entries.next_entry().await {
312 let ft = match entry.file_type().await {
313 Ok(ft) => ft,
314 Err(_) => continue,
315 };
316 if !ft.is_dir() {
317 continue;
318 }
319
320 let name = entry.file_name();
321 let name_str = name.to_string_lossy();
322
323 if is_ignored_dir(&name_str) {
325 continue;
326 }
327
328 let full_path = entry.path();
329 let pkg_json = full_path.join("package.json");
330 if fs::metadata(&pkg_json).await.is_ok() {
331 results.push(pkg_json);
332 }
333
334 Box::pin(search_recursive(&full_path, results)).await;
335 }
336}
337
338async fn find_nested_package_json_files(start_path: &Path) -> Vec<PackageJsonLocation> {
340 let mut results = Vec::new();
341 let root_pkg = start_path.join("package.json");
342 search_nested(start_path, &root_pkg, 0, &mut results).await;
343 results
344}
345
346async fn search_nested(
347 dir: &Path,
348 root_pkg: &Path,
349 depth: usize,
350 results: &mut Vec<PackageJsonLocation>,
351) {
352 if depth > 5 {
353 return;
354 }
355
356 let mut entries = match fs::read_dir(dir).await {
357 Ok(e) => e,
358 Err(_) => return,
359 };
360
361 while let Ok(Some(entry)) = entries.next_entry().await {
362 let ft = match entry.file_type().await {
363 Ok(ft) => ft,
364 Err(_) => continue,
365 };
366 if !ft.is_dir() {
367 continue;
368 }
369
370 let name = entry.file_name();
371 let name_str = name.to_string_lossy();
372
373 if is_ignored_dir(&name_str) {
374 continue;
375 }
376
377 let full_path = entry.path();
378 let pkg_json = full_path.join("package.json");
379 if fs::metadata(&pkg_json).await.is_ok() && pkg_json != root_pkg {
380 results.push(PackageJsonLocation {
381 path: pkg_json,
382 is_root: false,
383 is_workspace: false,
384 workspace_pattern: None,
385 });
386 }
387
388 Box::pin(search_nested(&full_path, root_pkg, depth + 1, results)).await;
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
399 fn test_parse_pnpm_basic() {
400 let yaml = "packages:\n - packages/*";
401 assert_eq!(parse_pnpm_workspace_patterns(yaml), vec!["packages/*"]);
402 }
403
404 #[test]
405 fn test_parse_pnpm_multiple_patterns() {
406 let yaml = "packages:\n - packages/*\n - apps/*\n - tools/*";
407 assert_eq!(
408 parse_pnpm_workspace_patterns(yaml),
409 vec!["packages/*", "apps/*", "tools/*"]
410 );
411 }
412
413 #[test]
414 fn test_parse_pnpm_quoted_patterns() {
415 let yaml = "packages:\n - 'packages/*'\n - \"apps/*\"";
416 assert_eq!(
417 parse_pnpm_workspace_patterns(yaml),
418 vec!["packages/*", "apps/*"]
419 );
420 }
421
422 #[test]
423 fn test_parse_pnpm_comments_interspersed() {
424 let yaml = "packages:\n # workspace packages\n - packages/*\n # apps\n - apps/*";
425 assert_eq!(
426 parse_pnpm_workspace_patterns(yaml),
427 vec!["packages/*", "apps/*"]
428 );
429 }
430
431 #[test]
432 fn test_parse_pnpm_empty_content() {
433 assert!(parse_pnpm_workspace_patterns("").is_empty());
434 }
435
436 #[test]
437 fn test_parse_pnpm_no_packages_key() {
438 let yaml = "name: my-project\nversion: 1.0.0";
439 assert!(parse_pnpm_workspace_patterns(yaml).is_empty());
440 }
441
442 #[test]
443 fn test_parse_pnpm_stops_at_next_section() {
444 let yaml = "packages:\n - packages/*\ncatalog:\n lodash: 4.17.21";
445 assert_eq!(parse_pnpm_workspace_patterns(yaml), vec!["packages/*"]);
446 }
447
448 #[test]
449 fn test_parse_pnpm_indented_key() {
450 let yaml = " packages:\n - packages/*";
452 assert_eq!(parse_pnpm_workspace_patterns(yaml), vec!["packages/*"]);
453 }
454
455 #[test]
456 fn test_parse_pnpm_dash_only_line() {
457 let yaml = "packages:\n -\n - packages/*";
458 assert_eq!(parse_pnpm_workspace_patterns(yaml), vec!["packages/*"]);
460 }
461
462 #[test]
463 fn test_parse_pnpm_glob_star_star() {
464 let yaml = "packages:\n - packages/**";
465 assert_eq!(parse_pnpm_workspace_patterns(yaml), vec!["packages/**"]);
466 }
467
468 #[tokio::test]
471 async fn test_detect_workspaces_npm_array() {
472 let dir = tempfile::tempdir().unwrap();
473 let pkg = dir.path().join("package.json");
474 fs::write(&pkg, r#"{"workspaces": ["packages/*"]}"#)
475 .await
476 .unwrap();
477 let config = detect_workspaces(&pkg).await;
478 assert!(matches!(config.ws_type, WorkspaceType::Npm));
479 assert_eq!(config.patterns, vec!["packages/*"]);
480 }
481
482 #[tokio::test]
483 async fn test_detect_workspaces_npm_object() {
484 let dir = tempfile::tempdir().unwrap();
485 let pkg = dir.path().join("package.json");
486 fs::write(
487 &pkg,
488 r#"{"workspaces": {"packages": ["packages/*", "apps/*"]}}"#,
489 )
490 .await
491 .unwrap();
492 let config = detect_workspaces(&pkg).await;
493 assert!(matches!(config.ws_type, WorkspaceType::Npm));
494 assert_eq!(config.patterns, vec!["packages/*", "apps/*"]);
495 }
496
497 #[tokio::test]
498 async fn test_detect_workspaces_pnpm() {
499 let dir = tempfile::tempdir().unwrap();
500 let pkg = dir.path().join("package.json");
501 fs::write(&pkg, r#"{"name": "root"}"#).await.unwrap();
502 let pnpm = dir.path().join("pnpm-workspace.yaml");
503 fs::write(&pnpm, "packages:\n - packages/*").await.unwrap();
504 let config = detect_workspaces(&pkg).await;
505 assert!(matches!(config.ws_type, WorkspaceType::Pnpm));
506 assert_eq!(config.patterns, vec!["packages/*"]);
507 }
508
509 #[tokio::test]
510 async fn test_detect_workspaces_pnpm_with_workspaces_field() {
511 let dir = tempfile::tempdir().unwrap();
514 let pkg = dir.path().join("package.json");
515 fs::write(&pkg, r#"{"name": "root", "workspaces": ["packages/*"]}"#)
516 .await
517 .unwrap();
518 let pnpm = dir.path().join("pnpm-workspace.yaml");
519 fs::write(&pnpm, "packages:\n - workspaces/*")
520 .await
521 .unwrap();
522 let config = detect_workspaces(&pkg).await;
523 assert!(matches!(config.ws_type, WorkspaceType::Pnpm));
524 assert_eq!(config.patterns, vec!["workspaces/*"]);
526 }
527
528 #[tokio::test]
529 async fn test_detect_workspaces_pnpm_with_malformed_package_json() {
530 let dir = tempfile::tempdir().unwrap();
534 let pkg = dir.path().join("package.json");
535 fs::write(&pkg, "{\n // a comment\n \"name\": \"root\"\n}")
537 .await
538 .unwrap();
539 let pnpm = dir.path().join("pnpm-workspace.yaml");
540 fs::write(&pnpm, "packages:\n - packages/*").await.unwrap();
541 let config = detect_workspaces(&pkg).await;
542 assert!(matches!(config.ws_type, WorkspaceType::Pnpm));
543 assert_eq!(config.patterns, vec!["packages/*"]);
544 }
545
546 #[tokio::test]
547 async fn test_detect_workspaces_none() {
548 let dir = tempfile::tempdir().unwrap();
549 let pkg = dir.path().join("package.json");
550 fs::write(&pkg, r#"{"name": "root"}"#).await.unwrap();
551 let config = detect_workspaces(&pkg).await;
552 assert!(matches!(config.ws_type, WorkspaceType::None));
553 assert!(config.patterns.is_empty());
554 }
555
556 #[tokio::test]
557 async fn test_detect_workspaces_invalid_json() {
558 let dir = tempfile::tempdir().unwrap();
559 let pkg = dir.path().join("package.json");
560 fs::write(&pkg, "not valid json!!!").await.unwrap();
561 let config = detect_workspaces(&pkg).await;
562 assert!(matches!(config.ws_type, WorkspaceType::None));
563 }
564
565 #[tokio::test]
566 async fn test_detect_workspaces_file_not_found() {
567 let dir = tempfile::tempdir().unwrap();
568 let pkg = dir.path().join("nonexistent.json");
569 let config = detect_workspaces(&pkg).await;
570 assert!(matches!(config.ws_type, WorkspaceType::None));
571 }
572
573 #[tokio::test]
574 async fn test_find_no_root_package_json() {
575 let dir = tempfile::tempdir().unwrap();
576 let result = find_package_json_files(dir.path()).await;
577 assert!(result.files.is_empty());
578 }
579
580 #[tokio::test]
581 async fn test_find_root_only() {
582 let dir = tempfile::tempdir().unwrap();
583 fs::write(dir.path().join("package.json"), r#"{"name":"root"}"#)
584 .await
585 .unwrap();
586 let result = find_package_json_files(dir.path()).await;
587 assert_eq!(result.files.len(), 1);
588 assert!(result.files[0].is_root);
589 }
590
591 #[tokio::test]
592 async fn test_find_npm_workspaces() {
593 let dir = tempfile::tempdir().unwrap();
594 fs::write(
595 dir.path().join("package.json"),
596 r#"{"workspaces": ["packages/*"]}"#,
597 )
598 .await
599 .unwrap();
600 let pkg_a = dir.path().join("packages").join("a");
601 fs::create_dir_all(&pkg_a).await.unwrap();
602 fs::write(pkg_a.join("package.json"), r#"{"name":"a"}"#)
603 .await
604 .unwrap();
605 let result = find_package_json_files(dir.path()).await;
606 assert!(matches!(result.workspace_type, WorkspaceType::Npm));
607 assert_eq!(result.files.len(), 2);
609 assert!(result.files[0].is_root);
610 assert!(result.files[1].is_workspace);
611 }
612
613 #[tokio::test]
614 async fn test_find_pnpm_workspaces() {
615 let dir = tempfile::tempdir().unwrap();
616 fs::write(dir.path().join("package.json"), r#"{"name":"root"}"#)
617 .await
618 .unwrap();
619 fs::write(
620 dir.path().join("pnpm-workspace.yaml"),
621 "packages:\n - packages/*",
622 )
623 .await
624 .unwrap();
625 let pkg_a = dir.path().join("packages").join("a");
626 fs::create_dir_all(&pkg_a).await.unwrap();
627 fs::write(pkg_a.join("package.json"), r#"{"name":"a"}"#)
628 .await
629 .unwrap();
630 let result = find_package_json_files(dir.path()).await;
631 assert!(matches!(result.workspace_type, WorkspaceType::Pnpm));
632 assert_eq!(result.files.len(), 2);
635 assert!(result.files[0].is_root);
636 assert!(result.files[1].is_workspace);
637 }
638
639 #[tokio::test]
640 async fn test_find_nested_skips_node_modules() {
641 let dir = tempfile::tempdir().unwrap();
642 fs::write(dir.path().join("package.json"), r#"{"name":"root"}"#)
643 .await
644 .unwrap();
645 let nm = dir.path().join("node_modules").join("lodash");
646 fs::create_dir_all(&nm).await.unwrap();
647 fs::write(nm.join("package.json"), r#"{"name":"lodash"}"#)
648 .await
649 .unwrap();
650 let result = find_package_json_files(dir.path()).await;
651 assert_eq!(result.files.len(), 1);
653 assert!(result.files[0].is_root);
654 }
655
656 #[tokio::test]
657 async fn test_find_nested_depth_limit() {
658 let dir = tempfile::tempdir().unwrap();
659 fs::write(dir.path().join("package.json"), r#"{"name":"root"}"#)
660 .await
661 .unwrap();
662 let mut deep = dir.path().to_path_buf();
664 for i in 0..7 {
665 deep = deep.join(format!("level{}", i));
666 }
667 fs::create_dir_all(&deep).await.unwrap();
668 fs::write(deep.join("package.json"), r#"{"name":"deep"}"#)
669 .await
670 .unwrap();
671 let result = find_package_json_files(dir.path()).await;
672 assert_eq!(result.files.len(), 1);
674 }
675
676 #[tokio::test]
677 async fn test_find_workspace_double_glob() {
678 let dir = tempfile::tempdir().unwrap();
679 fs::write(
680 dir.path().join("package.json"),
681 r#"{"workspaces": ["apps/**"]}"#,
682 )
683 .await
684 .unwrap();
685 let nested = dir.path().join("apps").join("web").join("client");
686 fs::create_dir_all(&nested).await.unwrap();
687 fs::write(nested.join("package.json"), r#"{"name":"client"}"#)
688 .await
689 .unwrap();
690 let result = find_package_json_files(dir.path()).await;
691 assert!(result.files.len() >= 2);
693 }
694
695 #[tokio::test]
696 async fn test_find_workspace_exact_path() {
697 let dir = tempfile::tempdir().unwrap();
698 fs::write(
699 dir.path().join("package.json"),
700 r#"{"workspaces": ["packages/core"]}"#,
701 )
702 .await
703 .unwrap();
704 let core = dir.path().join("packages").join("core");
705 fs::create_dir_all(&core).await.unwrap();
706 fs::write(core.join("package.json"), r#"{"name":"core"}"#)
707 .await
708 .unwrap();
709 let result = find_package_json_files(dir.path()).await;
710 assert_eq!(result.files.len(), 2);
711 }
712
713 #[test]
714 fn test_parse_pnpm_inline_comment_stripped() {
715 let yaml = "packages:\n - packages/* # workspace packages\n - apps/*\t# trailing tab";
717 assert_eq!(
718 parse_pnpm_workspace_patterns(yaml),
719 vec!["packages/*", "apps/*"]
720 );
721 }
722
723 #[test]
724 fn test_parse_pnpm_quoted_value_keeps_hash() {
725 let yaml = "packages:\n - 'packages/#weird' # but this is a comment";
727 assert_eq!(parse_pnpm_workspace_patterns(yaml), vec!["packages/#weird"]);
728 }
729
730 #[tokio::test]
731 async fn test_find_overlapping_patterns_no_duplicates() {
732 let dir = tempfile::tempdir().unwrap();
735 fs::write(
736 dir.path().join("package.json"),
737 r#"{"workspaces": ["packages/*", "packages/a"]}"#,
738 )
739 .await
740 .unwrap();
741 let a = dir.path().join("packages").join("a");
742 fs::create_dir_all(&a).await.unwrap();
743 fs::write(a.join("package.json"), r#"{"name":"a"}"#)
744 .await
745 .unwrap();
746 let result = find_package_json_files(dir.path()).await;
747 assert_eq!(result.files.len(), 2);
749 assert!(result.files[0].is_root);
750 let workspace_count = result.files.iter().filter(|f| f.is_workspace).count();
751 assert_eq!(workspace_count, 1);
752 }
753
754 #[tokio::test]
755 async fn test_find_star_pattern_skips_node_modules() {
756 let dir = tempfile::tempdir().unwrap();
759 fs::write(
760 dir.path().join("package.json"),
761 r#"{"workspaces": ["packages/*"]}"#,
762 )
763 .await
764 .unwrap();
765 let real = dir.path().join("packages").join("real");
766 fs::create_dir_all(&real).await.unwrap();
767 fs::write(real.join("package.json"), r#"{"name":"real"}"#)
768 .await
769 .unwrap();
770 for ignored in ["node_modules", ".cache", "dist", "build"] {
771 let d = dir.path().join("packages").join(ignored);
772 fs::create_dir_all(&d).await.unwrap();
773 fs::write(d.join("package.json"), r#"{"name":"x"}"#)
774 .await
775 .unwrap();
776 }
777 let result = find_package_json_files(dir.path()).await;
778 assert_eq!(result.files.len(), 2);
780 let workspace_count = result.files.iter().filter(|f| f.is_workspace).count();
781 assert_eq!(workspace_count, 1);
782 }
783
784 #[tokio::test]
785 async fn test_find_workspace_bare_star() {
786 let dir = tempfile::tempdir().unwrap();
789 fs::write(dir.path().join("package.json"), r#"{"workspaces": ["*"]}"#)
790 .await
791 .unwrap();
792 for member in ["a", "b"] {
793 let m = dir.path().join(member);
794 fs::create_dir_all(&m).await.unwrap();
795 fs::write(m.join("package.json"), r#"{"name":"m"}"#)
796 .await
797 .unwrap();
798 }
799 let nm = dir.path().join("node_modules").join("dep");
801 fs::create_dir_all(&nm).await.unwrap();
802 fs::write(nm.join("package.json"), r#"{"name":"dep"}"#)
803 .await
804 .unwrap();
805 let result = find_package_json_files(dir.path()).await;
806 let workspace_count = result.files.iter().filter(|f| f.is_workspace).count();
807 assert_eq!(workspace_count, 2);
809 assert!(result.files[0].is_root);
810 }
811
812 #[tokio::test]
813 async fn test_find_workspace_bare_double_glob() {
814 let dir = tempfile::tempdir().unwrap();
816 fs::write(dir.path().join("package.json"), r#"{"workspaces": ["**"]}"#)
817 .await
818 .unwrap();
819 let nested = dir.path().join("a").join("b");
820 fs::create_dir_all(&nested).await.unwrap();
821 fs::write(nested.join("package.json"), r#"{"name":"b"}"#)
822 .await
823 .unwrap();
824 let result = find_package_json_files(dir.path()).await;
825 let workspace_count = result.files.iter().filter(|f| f.is_workspace).count();
826 assert!(workspace_count >= 1);
827 }
828
829 #[tokio::test]
830 async fn test_find_workspace_deep_prefix_glob() {
831 let dir = tempfile::tempdir().unwrap();
834 fs::write(
835 dir.path().join("package.json"),
836 r#"{"workspaces": ["group/sub/*"]}"#,
837 )
838 .await
839 .unwrap();
840 let member = dir.path().join("group").join("sub").join("pkg");
841 fs::create_dir_all(&member).await.unwrap();
842 fs::write(member.join("package.json"), r#"{"name":"pkg"}"#)
843 .await
844 .unwrap();
845 let result = find_package_json_files(dir.path()).await;
846 let workspace_count = result.files.iter().filter(|f| f.is_workspace).count();
847 assert_eq!(workspace_count, 1);
848 }
849
850 #[tokio::test]
853 async fn test_detect_npm_by_default() {
854 let dir = tempfile::tempdir().unwrap();
855 let pm = detect_package_manager(dir.path()).await;
856 assert_eq!(pm, PackageManager::Npm);
857 }
858
859 #[tokio::test]
860 async fn test_detect_pnpm_lock_yaml() {
861 let dir = tempfile::tempdir().unwrap();
862 fs::write(dir.path().join("pnpm-lock.yaml"), "lockfileVersion: 9.0\n")
863 .await
864 .unwrap();
865 let pm = detect_package_manager(dir.path()).await;
866 assert_eq!(pm, PackageManager::Pnpm);
867 }
868
869 #[tokio::test]
870 async fn test_detect_pnpm_workspace_yaml() {
871 let dir = tempfile::tempdir().unwrap();
872 fs::write(
873 dir.path().join("pnpm-workspace.yaml"),
874 "packages:\n - packages/*",
875 )
876 .await
877 .unwrap();
878 let pm = detect_package_manager(dir.path()).await;
879 assert_eq!(pm, PackageManager::Pnpm);
880 }
881}