Project detection

Source: src/engine/project-detector.ts

Detects whether the target is a monorepo or a single project, and extracts project metadata: NestJS version, ORM, and HTTP framework. Detection runs early so the pipeline knows whether to loop over sub-projects or scan once. The metadata is shown in the report and read by ORM-specific rules like no-orm-in-controllers and no-raw-entity-in-response.

Monorepo detection

Input is the root directory to check:

targetPath: string    // root directory to check

The result is a map of sub-projects, or null:

interface MonorepoInfo {
  projects: Map<string, string>   // project name → relative root path
}
// Returns null if not a monorepo

Five detection strategies are tried in priority order. The first match wins.

Strategy 1: nest-cli.json

Reads nest-cli.json and checks for:

  • monorepo: true
  • A projects map with at least one entry

A matching file looks like this:

{
  "monorepo": true,
  "projects": {
    "api": { "root": "apps/api" },
    "admin": { "root": "apps/admin" }
  }
}

Strategy 2: pnpm-workspace.yaml

Reads pnpm-workspace.yaml and:

  1. Parses the packages: patterns (block-style and inline arrays are both supported)
  2. Globs for package.json files matching those patterns
  3. Filters to packages that have @nestjs/core or @nestjs/common in dependencies, devDependencies, or peerDependencies

The patterns it reads:

packages:
  - "apps/*"
  - "packages/*"

Strategy 3: package.json workspaces (npm / Yarn)

If no pnpm-workspace.yaml exists, reads the workspaces field from the root package.json. Both the array form and the object form (Yarn classic) are supported:

{
  "workspaces": ["apps/*", "packages/*"]
}

The object form nests the globs under packages:

{
  "workspaces": {
    "packages": ["apps/*", "packages/*"]
  }
}

Workspace globs resolve the same way as in strategy 2, filtering to packages with @nestjs/core or @nestjs/common. This strategy is skipped when pnpm-workspace.yaml exists, because pnpm repos may have both and would otherwise be detected twice.

Strategy 4: nx.json (Nx)

Detects the presence of nx.json, then scans for project.json files throughout the repo (excluding node_modules and the root-level project.json). For each project directory with a project.json, reads the sibling package.json. The project is included when that file depends on @nestjs/core or @nestjs/common.

A failed package.json check does not end it. Nx projects declare their dependencies at the workspace root, so many have no package.json at all. The strategy then globs the project's **/*.module.ts files and includes the project when one of them contains the text @nestjs/common. It is a substring match on the file, not an import check, and it runs for a project whose package.json exists without a Nest dependency too.

Strategy 5: lerna.json (standalone Lerna)

Reads lerna.json and uses its packages globs, defaulting to ["packages/*"] when they are not specified. Skipped when useWorkspaces is true. Lerna then delegates to npm or Yarn workspaces, so strategy 3 handles detection instead.

A minimal lerna.json:

{
  "packages": ["packages/*"]
}

Fallback warning

Some repos carry monorepo markers but no NestJS package. The markers are lerna.json, turbo.json, nx.json, pnpm-workspace.yaml, and a workspaces field in package.json. When a marker is present and no strategy finds a NestJS package, nestjs-doctor warns and falls back to single-project mode.

If a strategy does match, the pipeline runs stages 1-9 independently for each sub-project, then aggregates the results into a combined score.

Single project detection

Input is the project root:

targetPath: string    // project root directory

The result is the project metadata:

interface ProjectInfo {
  name: string                       // from package.json name, or "unknown"
  nestVersion: string | null         // installed version, else the declared one
  orm: string | null                 // detected ORM
  framework: "express" | "fastify" | null
  moduleCount: number                // 0 initially, updated by scanner
  fileCount: number                  // 0 initially, updated by scanner
}

How it works

Reads the project's package.json and inspects dependencies plus devDependencies. An ORM that shows as null is missing from both.

ORM detection takes the first match:

  1. @prisma/client"prisma"
  2. typeorm"typeorm"
  3. @mikro-orm/core"mikro-orm"
  4. sequelize"sequelize"
  5. mongoose"mongoose"
  6. drizzle-orm"drizzle"

Framework detection:

  • @nestjs/platform-fastify"fastify"
  • @nestjs/platform-express or @nestjs/core"express"

The NestJS version is read from node_modules/@nestjs/core, walking up from the scanned path so a hoisted workspace install is found. The walk stops at the first directory holding a .git.

With nothing installed it falls back to the first version the declared spec names, so ^10.0.0 reads as 10.0.0. A spec naming no version, such as workspace:*, leaves nestVersion as null.