( repoRoot: string, workspaces: WorkspaceInfo[] )
| 360 | } |
| 361 | |
| 362 | async function discoverImplicitWorkspaces( |
| 363 | repoRoot: string, |
| 364 | workspaces: WorkspaceInfo[] |
| 365 | ): Promise<void> { |
| 366 | // Roku multi-channel monorepo: signal requires roku-deploy + a |
| 367 | // `common/` + >=2 sibling-channel structure. Won't fire on the 90% single- |
| 368 | // channel case or on non-Roku repos that happen to have a `manifest` file. |
| 369 | try { |
| 370 | const rokuMono = await detectRokuMonorepo(repoRoot); |
| 371 | if (rokuMono) { |
| 372 | // Each channel dir becomes a workspace. The `common/` dir isn't a |
| 373 | // shippable channel, but it contains the bulk of the shared code — |
| 374 | // register it as a workspace too so its components/schemas show up. |
| 375 | const dirs = [rokuMono.commonDir, ...rokuMono.channelDirs]; |
| 376 | for (const dir of dirs) { |
| 377 | const wsInfo = await detectNonJSWorkspace(repoRoot, dir, basename(dir)); |
| 378 | if (wsInfo && !workspaces.some((w) => w.path === wsInfo.path)) { |
| 379 | workspaces.push(wsInfo); |
| 380 | } |
| 381 | } |
| 382 | } |
| 383 | } catch {} |
| 384 | |
| 385 | try { |
| 386 | const topDirs = await readdir(repoRoot, { withFileTypes: true }); |
| 387 | for (const d of topDirs) { |
| 388 | if (!d.isDirectory() || d.name.startsWith(".") || IGNORE_DIRS.has(d.name)) continue; |
| 389 | const wsPath = join(repoRoot, d.name); |
| 390 | |
| 391 | if (await hasDirectWorkspaceManifest(wsPath)) { |
| 392 | const wsInfo = await detectWorkspace(repoRoot, wsPath, d.name); |
| 393 | if (wsInfo && !workspaces.some((w) => w.path === wsInfo.path)) { |
| 394 | workspaces.push(wsInfo); |
| 395 | } |
| 396 | } |
| 397 | |
| 398 | // Depth-2 discovery — always runs, not as fallback. Catches nested |
| 399 | // workspaces in container dirs (e.g. `repos/Engine/`, `apps/web/`, |
| 400 | // `services/api/`, `container-dir/backend/`) even when the container |
| 401 | // itself also matched via subdirectory manifest detection. |
| 402 | try { |
| 403 | const nestedDirs = await readdir(wsPath, { withFileTypes: true }); |
| 404 | for (const n of nestedDirs) { |
| 405 | if (!n.isDirectory() || n.name.startsWith(".") || IGNORE_DIRS.has(n.name)) continue; |
| 406 | const nestedPath = join(wsPath, n.name); |
| 407 | if (!(await hasDirectWorkspaceManifest(nestedPath))) continue; |
| 408 | const nestedInfo = await detectWorkspace(repoRoot, nestedPath, n.name); |
| 409 | if (nestedInfo && !workspaces.some((w) => w.path === nestedInfo.path)) { |
| 410 | workspaces.push(nestedInfo); |
| 411 | } |
| 412 | } |
| 413 | } catch {} |
| 414 | } |
| 415 | } catch {} |
| 416 | } |
| 417 | |
| 418 | /** |
| 419 | * Classify the repo structure into one of: single, monorepo, microservices, meta. |
no test coverage detected