{"id":"9e0878301e18d1d7","repo":"vitest-dev/vitest","slug":"found-a-circular-projects-definition-chai","errorCode":null,"errorMessage":"Found a circular \"projects\" definition: ${[...chain, realConfigFile].map(file => `\"${relative(context.rootConfig.root, file)}\"`).join(' -> ')}. Make sure your configuration is correct.","messagePattern":"Found a circular \"projects\" definition: (.+?)\"`\\)\\.join\\(' -> '\\)\\}\\. Make sure your configuration is correct\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/vitest/src/node/projects/resolveProjects.ts","lineNumber":429,"sourceCode":"  for (const entry of entries) {\n    const definitions = entry.projectConfig.projects\n    // inline projects cannot declare `projects`; the declaring config's own\n    // entry (emitted when it references its own config file) is kept as-is —\n    // its `projects` are the definitions currently being resolved\n    if (entry.inline || definitions === undefined || entry.projectConfig === context.parentConfig) {\n      result.push(entry)\n      continue\n    }\n\n    const configFile = entry.viteConfig.configFile\n    const relativeFile = configFile\n      ? relative(context.rootConfig.root, configFile)\n      : entry.projectConfig.name\n    let chain = context.chain\n    if (configFile) {\n      const realConfigFile = safeRealpath(configFile)\n      if (chain.includes(realConfigFile)) {\n        throw new Error(\n          [\n            `Found a circular \"projects\" definition: `,\n            [...chain, realConfigFile].map(file => `\"${relative(context.rootConfig.root, file)}\"`).join(' -> '),\n            '. Make sure your configuration is correct.',\n          ].join(''),\n        )\n      }\n      chain = [...chain, realConfigFile]\n      context.containerConfigFiles.push(configFile)\n    }\n\n    const childContext: ProjectsResolutionContext = {\n      ...context,\n      parentViteConfig: entry.viteConfig,\n      parentConfig: entry.projectConfig,\n      ancestors: [...context.ancestors, entry.projectConfig.name],\n      chain,\n    }","sourceCodeStart":411,"sourceCodeEnd":447,"githubUrl":"https://github.com/vitest-dev/vitest/blob/d568f8ce3739b532d5bf2c1ee1e45e8a8a473d09/packages/vitest/src/node/projects/resolveProjects.ts#L411-L447","documentation":"flattenContainerEntries (resolveProjects.ts:426-436) walks each container config (a file-based config that itself declares projects). It maintains a chain of realpath'd config files; if a container's file is already in the chain, the projects definitions form a cycle and Vitest throws, printing the cycle as file -> file -> file.","triggerScenarios":"Config A's projects array references config B, and config B's projects array references config A; a config lists itself in its own projects; a chain A -> B -> C -> A.","commonSituations":"Refactoring workspace files into projects arrays and accidentally creating a back-reference; two configs meant to extend each other via `extends` but wired through `projects` instead; copy-paste of a projects array that includes the source file.","solutions":["Inspect the printed cycle and break it by removing the back-reference.","Use `extends` (single-parent inheritance) instead of mutual `projects` references for shared config.","Centralize shared config in a base file that both projects extend, rather than referencing each other.","If a config should also run as a project, list it once from the root and don't re-list it from a child."],"exampleFix":"// before: a.config.ts projects: ['./b.config.ts']; b.config.ts projects: ['./a.config.ts']\n\n// after: shared base, both extend it\n// base.config.ts exports common test config\n// a.config.ts: export default defineConfig([{ test: { extends: './base.config.ts', ... } }])","handlingStrategy":"validation","validationCode":"function detectCycle(adjacency: Map<string, string[]>, start: string): string[] | null {\n  const seen = new Set<string>()\n  const stack: string[] = []\n  function visit(node: string): string[] | null {\n    if (stack.includes(node)) return [...stack.slice(stack.indexOf(node)), node]\n    if (seen.has(node)) return null\n    seen.add(node); stack.push(node)\n    for (const next of adjacency.get(node) ?? []) if (visit(next)) return visit(next)\n    stack.pop(); return null\n  }\n  return visit(start)\n}","typeGuard":"function hasCycle(files: string[], refs: Map<string, string[]>): boolean {\n  return detectCycle(refs, files[0]) !== null\n}","tryCatchPattern":null,"preventionTips":["Use `extends` for one-way inheritance; reserve `projects` for true composition.","Keep a single root that lists children; avoid children listing each other.","Lint against a config file appearing in its own projects array."],"tags":["projects","circular","configuration","container","recursion"],"analyzedSha":"d568f8ce3739b532d5bf2c1ee1e45e8a8a473d09","analyzedAt":"2026-08-03T20:23:56.861Z","schemaVersion":2}