vitest-dev/vitest · error · Error

Found a circular "projects" definition: "${file}" -> "${file

Error message

Found a circular "projects" definition: "${file}" -> "${file}". Make sure your configuration is correct.

What it means

When flattening container entries (config files that themselves declare 'projects'), Vitest tracks the chain of configFile paths (realpath-resolved). If a config file's realpath already appears in the current chain, resolution is cyclic and would loop forever, so it throws listing the full chain.

Source

Thrown at packages/vitest/src/node/projects/resolveProjects.ts:442

  for (const entry of entries) {
    const definitions = entry.projectConfig.projects
    // inline projects cannot declare `projects`; the declaring config's own
    // entry (emitted when it references its own config file) is kept as-is —
    // its `projects` are the definitions currently being resolved
    if (entry.inline || definitions === undefined || entry.projectConfig === context.parentConfig) {
      result.push(entry)
      continue
    }

    const configFile = entry.viteConfig.configFile
    const relativeFile = configFile
      ? relative(context.rootConfig.root, configFile)
      : entry.projectConfig.name
    let chain = context.chain
    if (configFile) {
      const realConfigFile = safeRealpath(configFile)
      if (chain.includes(realConfigFile)) {
        throw new Error(
          [
            `Found a circular "projects" definition: `,
            [...chain, realConfigFile].map(file => `"${relative(context.rootConfig.root, file)}"`).join(' -> '),
            '. Make sure your configuration is correct.',
          ].join(''),
        )
      }
      chain = [...chain, realConfigFile]
      context.containerConfigFiles.push(configFile)
    }

    const childContext: ProjectsResolutionContext = {
      ...context,
      parentViteConfig: entry.viteConfig,
      parentConfig: entry.projectConfig,
      ancestors: [...context.ancestors, entry.projectConfig.name],
      chain,
    }

View on GitHub (pinned to 1fa9837ec2)

Solutions

  1. Read the chain in the error to see which configs form the cycle.
  2. Break the cycle by removing the back-reference in one of the configs.
  3. Avoid globs that re-include a config file that already includes children.
  4. Keep container/leaf roles separate: containers declare projects, leaves do not re-include containers.

Example fix

// before - a.workspace.ts includes b.config.ts which includes a.workspace.ts
// after - b.config.ts declares only leaf projects, no back-reference
Defensive patterns

Strategy: validation

Validate before calling

// Detect cycles in a projects graph before resolution.
function detectCycle(graph: Map<string, string[]>, start: string) {
  const stack: string[] = []
  const dfs = (node: string): boolean => {
  if (stack.includes(node)) throw new Error(`Cycle: ${[...stack, node].join(' -> ')}`)
  stack.push(node)
  for (const next of graph.get(node) ?? []) if (dfs(next)) return true
  stack.pop()
  return false
  }
  dfs(start)
}

Prevention

When it happens

Trigger: Config file A declares projects including config file B, which (directly or transitively) declares projects including config file A again, forming a cycle in the realpath chain.

Common situations: Workspace files that reference each other, a project glob that re-includes the parent config, or a copy-pasted config that points back to its own container.

Related errors


AI-assisted analysis of vitest-dev/vitest@1fa9837ec2 (2026-08-11). Data as JSON: /api/errors/22b5946cff3c6b4a. Report an issue: GitHub.