CherryHQ/cherry-studio · error · Error
[theme-contract] ${mode} variable cycle: ${[...stack.slice(c
Error message
[theme-contract] ${mode} variable cycle: ${[...stack.slice(cycleStart), name].join(' -> ')} What it means
Thrown by assertNoCycles when the var() reference graph in a mode contains a cycle (A -> B -> ... -> A). The validator does a DFS over references; revisiting a node currently on the visit stack is a cycle. The message prints the cycle path so you can see exactly which variables point at each other.
Source
Thrown at packages/ui/scripts/validate-theme-contract.ts:204
if (!declarations.has(reference)) {
throw new Error(
`[theme-contract] ${mode} ${declaration.name} in ${declaration.source} references undefined ${reference}`
)
}
}
}
}
function assertNoCycles(mode: string, declarations: Map<string, Declaration>): void {
const visited = new Set<string>()
const visiting = new Set<string>()
const stack: string[] = []
const visit = (name: string): void => {
if (visited.has(name)) return
if (visiting.has(name)) {
const cycleStart = stack.indexOf(name)
throw new Error(`[theme-contract] ${mode} variable cycle: ${[...stack.slice(cycleStart), name].join(' -> ')}`)
}
visiting.add(name)
stack.push(name)
const declaration = declarations.get(name)
if (declaration) {
for (const reference of extractReferences(declaration.value, declaration.source)) {
if (declarations.has(reference)) visit(reference)
}
}
stack.pop()
visiting.delete(name)
visited.add(name)
}
for (const name of declarations.keys()) visit(name)View on GitHub (pinned to 726446b54c)
Solutions
- Inspect the cycle path in the message and re-point at least one declaration in the cycle at a concrete value or a variable OUTSIDE the cycle.
- Pick a single owner for the underlying value and have the other variable(s) reference it one-directionally.
- Re-run `pnpm --filter @cherrystudio/ui theme:check` and the reference/cycle assertions together.
Example fix
// before (product.css) --inline-code: var(--inline-code-foreground); --inline-code-foreground: var(--inline-code); // after — one-directional --inline-code: rgba(0, 0, 0, 0.06); --inline-code-foreground: rgb(218, 97, 92);
Defensive patterns
Strategy: validation
Validate before calling
// Detect cycles via DFS over the var() reference graph before calling the validator.
function hasCycle(decls: Map<string, string>) {
const color = new Map<string, 0 | 1 | 2>()
const dfs = (n: string): boolean => {
color.set(n, 1)
for (const [, ref] of (decls.get(n) ?? '').matchAll(/var\(\s*(--[a-z0-9-]+)/g)) {
if (!decls.has(ref)) continue
if (color.get(ref) === 1) return true
if (color.get(ref) === undefined && dfs(ref)) return true
}
color.set(n, 2)
return false
}
return [...decls.keys()].some((n) => color.get(n) === undefined && dfs(n))
} Try / catch
try {
validateThemeContractSources(sources)
} catch (error) {
if (error instanceof Error && /variable cycle/.test(error.message)) {
console.error(error.message) // prints the cycle path
process.exitCode = 1
return
}
throw error
} Prevention
- When swapping which variable is the source of truth, ensure the old back-reference is removed.
- Prefer one-directional chains: one concrete owner, others reference it.
- Run theme:check after rewiring var() references.
When it happens
Trigger: `--inline-code: var(--inline-code-foreground);` together with `--inline-code-foreground: var(--inline-code);` in product.css; rewiring two variables to point at each other during a dark-mode refactor; a chain like `--a: var(--b); --b: var(--c); --c: var(--a);`.
Common situations: Swapping which variable is the 'source of truth' and forgetting to break the old back-reference; merge conflicts that combine two half-refactors.
Related errors
- [theme-contract] ${declaration.name} is defined twice in ${s
- [theme-contract] ${label} is missing root declarations: ${mi
- [theme-contract] ${label} references missing foundation vari
- [theme-contract] ${mode} ${declaration.name} in ${declaratio
- [theme-contract] foundation cannot declare runtime input ${d
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/706b7181c60aeeb3.
Report an issue: GitHub.