vitest-dev/vitest · error · Error

Found a circular "projects" definition

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.

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.

Appendix: 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)