2121
2222import tomlkit
2323
24+ _BUILD_DIRS = frozenset ({"build" , "dist" , "out" , ".next" , ".nuxt" })
25+
2426
2527@dataclass
2628class DetectedProject :
@@ -310,14 +312,21 @@ def _detect_js_module_root(project_root: Path) -> tuple[Path, str]:
310312 """Detect JavaScript/TypeScript module root.
311313
312314 Priority:
313- 1. package.json "exports" field
314- 2. package.json "module" field (ESM)
315- 3. package.json "main" field (CJS)
316- 4. src/ directory
317- 5. lib/ directory
318- 6. Project root
315+ 1. src/, lib/, source/ directories (common source directories)
316+ 2. package.json "exports" field (if not in build output directory)
317+ 3. package.json "module" field (ESM, if not in build output directory)
318+ 4. package.json "main" field (CJS, if not in build output directory)
319+ 5. Project root
320+
321+ Build output directories (build/, dist/, out/) are skipped since they contain
322+ compiled code, not source files.
319323
320324 """
325+ # Check for common source directories first - these are always preferred
326+ for src_dir in ["src" , "lib" , "source" ]:
327+ if (project_root / src_dir ).is_dir ():
328+ return project_root / src_dir , f"{ src_dir } / directory"
329+
321330 package_json_path = project_root / "package.json"
322331 package_data : dict [str , Any ] = {}
323332
@@ -334,32 +343,52 @@ def _detect_js_module_root(project_root: Path) -> tuple[Path, str]:
334343 entry_path = _extract_entry_path (exports )
335344 if entry_path :
336345 parent = Path (entry_path ).parent
337- if parent != Path () and parent .as_posix () != "." and (project_root / parent ).is_dir ():
346+ if (
347+ parent != Path ()
348+ and parent .as_posix () != "."
349+ and (project_root / parent ).is_dir ()
350+ and not is_build_output_dir (parent )
351+ ):
338352 return project_root / parent , f'{ parent .as_posix ()} / (from package.json "exports")'
339353
340354 # Check module field (ESM)
341355 module_field = package_data .get ("module" )
342356 if module_field and isinstance (module_field , str ):
343357 parent = Path (module_field ).parent
344- if parent != Path () and parent .as_posix () != "." and (project_root / parent ).is_dir ():
358+ if (
359+ parent != Path ()
360+ and parent .as_posix () != "."
361+ and (project_root / parent ).is_dir ()
362+ and not is_build_output_dir (parent )
363+ ):
345364 return project_root / parent , f'{ parent .as_posix ()} / (from package.json "module")'
346365
347366 # Check main field (CJS)
348367 main_field = package_data .get ("main" )
349368 if main_field and isinstance (main_field , str ):
350369 parent = Path (main_field ).parent
351- if parent != Path () and parent .as_posix () != "." and (project_root / parent ).is_dir ():
370+ if (
371+ parent != Path ()
372+ and parent .as_posix () != "."
373+ and (project_root / parent ).is_dir ()
374+ and not is_build_output_dir (parent )
375+ ):
352376 return project_root / parent , f'{ parent .as_posix ()} / (from package.json "main")'
353377
354- # Check for common source directories
355- for src_dir in ["src" , "lib" , "source" ]:
356- if (project_root / src_dir ).is_dir ():
357- return project_root / src_dir , f"{ src_dir } / directory"
358-
359378 # Default to project root
360379 return project_root , "project root"
361380
362381
382+ def is_build_output_dir (path : Path ) -> bool :
383+ """Check if a path is within a common build output directory.
384+
385+ Build output directories contain compiled code and should be skipped
386+ in favor of source directories.
387+
388+ """
389+ return not _BUILD_DIRS .isdisjoint (path .parts )
390+
391+
363392def _extract_entry_path (exports : Any ) -> str | None :
364393 """Extract entry path from package.json exports field."""
365394 if isinstance (exports , str ):
0 commit comments