Navegador

feat: detect bare monorepos without workspace tooling Adds a heuristic fallback to WorkspaceDetector so repos that are just a collection of apps/services (no Turborepo, Nx, pnpm, Cargo workspace, etc.) are still detected and ingested as a monorepo. - _bare_packages(): scans one level down, qualifies dirs that contain any known project manifest (package.json, pyproject.toml, Cargo.toml, go.mod, pom.xml, Gemfile, etc.) - _has_nested_manifest(): secondary check one level deeper for Django/WSGI markers (manage.py, wsgi.py, asgi.py) which often live inside an inner package dir (e.g. consult/djangosite/manage.py) - _read_package_deps() now handles workspace_type='bare' by trying all manifest parsers and merging, so inter-package DEPENDS_ON edges still get created in mixed-language repos - Threshold of 2+ qualifying dirs required to avoid false positives

lmata 2026-04-02 02:24 trunk
Commit 0f42b61a60cfe463ead0fa09c5ddc68f653dcec46b187c46aada91d264782ff1
1 file changed +73
--- navegador/monorepo.py
+++ navegador/monorepo.py
@@ -106,10 +106,15 @@
106106
# Go workspace
107107
if (root / "go.work").exists():
108108
packages = self._go_packages(root)
109109
return WorkspaceConfig(type="go", root=root, packages=packages)
110110
111
+ # Bare monorepo — no tooling, just a directory of apps/services
112
+ packages = self._bare_packages(root)
113
+ if len(packages) >= 2:
114
+ return WorkspaceConfig(type="bare", root=root, packages=packages)
115
+
111116
return None
112117
113118
# ── JS-family helpers ─────────────────────────────────────────────────────
114119
115120
def _js_workspace_packages(self, root: Path) -> list[Path]:
@@ -220,10 +225,71 @@
220225
for child in sorted(root.iterdir()):
221226
if child.is_dir() and not child.name.startswith("."):
222227
if (child / "package.json").exists():
223228
packages.append(child)
224229
return packages
230
+
231
+ # ── Bare monorepo helpers ─────────────────────────────────────────────────
232
+
233
+ # Manifests expected directly inside the package root
234
+ _PROJECT_MANIFESTS = (
235
+ "package.json",
236
+ "pyproject.toml",
237
+ "setup.py",
238
+ "setup.cfg",
239
+ "Cargo.toml",
240
+ "go.mod",
241
+ "pom.xml",
242
+ "build.gradle",
243
+ "build.gradle.kts",
244
+ "Gemfile",
245
+ "composer.json",
246
+ "mix.exs",
247
+ )
248
+
249
+ # Manifests that may live one subdirectory deeper (e.g. Django's manage.py
250
+ # inside an inner package dir: myapp/myapp/manage.py)
251
+ _NESTED_MANIFESTS = (
252
+ "manage.py",
253
+ "wsgi.py",
254
+ "asgi.py",
255
+ )
256
+
257
+ def _bare_packages(self, root: Path) -> list[Path]:
258
+ """
259
+ Detect a bare monorepo: a directory whose immediate children are
260
+ independent apps/services with no shared workspace tooling.
261
+
262
+ A child directory qualifies if it contains at least one recognised
263
+ project manifest directly, or a Django/WSGI manifest one level deeper
264
+ (e.g. myapp/myapp/manage.py).
265
+ Non-project dirs (docs, scripts, config-only folders) are skipped.
266
+ """
267
+ packages: list[Path] = []
268
+ for child in sorted(root.iterdir()):
269
+ if not child.is_dir() or child.name.startswith("."):
270
+ continue
271
+ # Check top-level manifests first
272
+ for manifest in self._PROJECT_MANIFESTS:
273
+ if (child / manifest).exists():
274
+ packages.append(child)
275
+ break
276
+ else:
277
+ # Fall back: look one level deeper for Django/WSGI markers
278
+ if self._has_nested_manifest(child):
279
+ packages.append(child)
280
+ return packages
281
+
282
+ def _has_nested_manifest(self, pkg_root: Path) -> bool:
283
+ """Return True if any immediate subdirectory contains a nested manifest."""
284
+ for subdir in pkg_root.iterdir():
285
+ if not subdir.is_dir() or subdir.name.startswith("."):
286
+ continue
287
+ for manifest in self._NESTED_MANIFESTS:
288
+ if (subdir / manifest).exists():
289
+ return True
290
+ return False
225291
226292
# ── Cargo helpers ─────────────────────────────────────────────────────────
227293
228294
def _cargo_packages(self, root: Path, cargo_toml: Path) -> list[Path] | None:
229295
"""
@@ -459,10 +525,17 @@
459525
return self._js_deps(pkg_path)
460526
if workspace_type == "cargo":
461527
return self._cargo_deps(pkg_path)
462528
if workspace_type == "go":
463529
return self._go_deps(pkg_path)
530
+ if workspace_type == "bare":
531
+ # Try all known manifest parsers and merge results
532
+ deps: list[str] = []
533
+ deps.extend(self._js_deps(pkg_path))
534
+ deps.extend(self._cargo_deps(pkg_path))
535
+ deps.extend(self._go_deps(pkg_path))
536
+ return deps
464537
return []
465538
466539
def _js_deps(self, pkg_path: Path) -> list[str]:
467540
pkg_json = pkg_path / "package.json"
468541
if not pkg_json.exists():
469542
--- navegador/monorepo.py
+++ navegador/monorepo.py
@@ -106,10 +106,15 @@
106 # Go workspace
107 if (root / "go.work").exists():
108 packages = self._go_packages(root)
109 return WorkspaceConfig(type="go", root=root, packages=packages)
110
 
 
 
 
 
111 return None
112
113 # ── JS-family helpers ─────────────────────────────────────────────────────
114
115 def _js_workspace_packages(self, root: Path) -> list[Path]:
@@ -220,10 +225,71 @@
220 for child in sorted(root.iterdir()):
221 if child.is_dir() and not child.name.startswith("."):
222 if (child / "package.json").exists():
223 packages.append(child)
224 return packages
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
226 # ── Cargo helpers ─────────────────────────────────────────────────────────
227
228 def _cargo_packages(self, root: Path, cargo_toml: Path) -> list[Path] | None:
229 """
@@ -459,10 +525,17 @@
459 return self._js_deps(pkg_path)
460 if workspace_type == "cargo":
461 return self._cargo_deps(pkg_path)
462 if workspace_type == "go":
463 return self._go_deps(pkg_path)
 
 
 
 
 
 
 
464 return []
465
466 def _js_deps(self, pkg_path: Path) -> list[str]:
467 pkg_json = pkg_path / "package.json"
468 if not pkg_json.exists():
469
--- navegador/monorepo.py
+++ navegador/monorepo.py
@@ -106,10 +106,15 @@
106 # Go workspace
107 if (root / "go.work").exists():
108 packages = self._go_packages(root)
109 return WorkspaceConfig(type="go", root=root, packages=packages)
110
111 # Bare monorepo — no tooling, just a directory of apps/services
112 packages = self._bare_packages(root)
113 if len(packages) >= 2:
114 return WorkspaceConfig(type="bare", root=root, packages=packages)
115
116 return None
117
118 # ── JS-family helpers ─────────────────────────────────────────────────────
119
120 def _js_workspace_packages(self, root: Path) -> list[Path]:
@@ -220,10 +225,71 @@
225 for child in sorted(root.iterdir()):
226 if child.is_dir() and not child.name.startswith("."):
227 if (child / "package.json").exists():
228 packages.append(child)
229 return packages
230
231 # ── Bare monorepo helpers ─────────────────────────────────────────────────
232
233 # Manifests expected directly inside the package root
234 _PROJECT_MANIFESTS = (
235 "package.json",
236 "pyproject.toml",
237 "setup.py",
238 "setup.cfg",
239 "Cargo.toml",
240 "go.mod",
241 "pom.xml",
242 "build.gradle",
243 "build.gradle.kts",
244 "Gemfile",
245 "composer.json",
246 "mix.exs",
247 )
248
249 # Manifests that may live one subdirectory deeper (e.g. Django's manage.py
250 # inside an inner package dir: myapp/myapp/manage.py)
251 _NESTED_MANIFESTS = (
252 "manage.py",
253 "wsgi.py",
254 "asgi.py",
255 )
256
257 def _bare_packages(self, root: Path) -> list[Path]:
258 """
259 Detect a bare monorepo: a directory whose immediate children are
260 independent apps/services with no shared workspace tooling.
261
262 A child directory qualifies if it contains at least one recognised
263 project manifest directly, or a Django/WSGI manifest one level deeper
264 (e.g. myapp/myapp/manage.py).
265 Non-project dirs (docs, scripts, config-only folders) are skipped.
266 """
267 packages: list[Path] = []
268 for child in sorted(root.iterdir()):
269 if not child.is_dir() or child.name.startswith("."):
270 continue
271 # Check top-level manifests first
272 for manifest in self._PROJECT_MANIFESTS:
273 if (child / manifest).exists():
274 packages.append(child)
275 break
276 else:
277 # Fall back: look one level deeper for Django/WSGI markers
278 if self._has_nested_manifest(child):
279 packages.append(child)
280 return packages
281
282 def _has_nested_manifest(self, pkg_root: Path) -> bool:
283 """Return True if any immediate subdirectory contains a nested manifest."""
284 for subdir in pkg_root.iterdir():
285 if not subdir.is_dir() or subdir.name.startswith("."):
286 continue
287 for manifest in self._NESTED_MANIFESTS:
288 if (subdir / manifest).exists():
289 return True
290 return False
291
292 # ── Cargo helpers ─────────────────────────────────────────────────────────
293
294 def _cargo_packages(self, root: Path, cargo_toml: Path) -> list[Path] | None:
295 """
@@ -459,10 +525,17 @@
525 return self._js_deps(pkg_path)
526 if workspace_type == "cargo":
527 return self._cargo_deps(pkg_path)
528 if workspace_type == "go":
529 return self._go_deps(pkg_path)
530 if workspace_type == "bare":
531 # Try all known manifest parsers and merge results
532 deps: list[str] = []
533 deps.extend(self._js_deps(pkg_path))
534 deps.extend(self._cargo_deps(pkg_path))
535 deps.extend(self._go_deps(pkg_path))
536 return deps
537 return []
538
539 def _js_deps(self, pkg_path: Path) -> list[str]:
540 pkg_json = pkg_path / "package.json"
541 if not pkg_json.exists():
542

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button