CherryHQ/cherry-studio · error · Error

[theme-contract] ${sourceName} declares invalid custom prope

Error message

[theme-contract] ${sourceName} declares invalid custom property ${name}

What it means

Thrown by the theme contract validator (validate-theme-contract.ts) when a CSS source file declares a custom property whose name does not match the pattern ^--[a-z0-9-]+$. This means the property name contains uppercase letters, underscores, special characters, or doesn't start with --. The validator enforces a strict kebab-case naming convention for all CSS custom properties in the theme system.

Source

Thrown at packages/ui/scripts/validate-theme-contract.ts:61

}

type SourceEntry = readonly [source: string, css: string]

function stripComments(source: string): string {
  return source.replace(/\/\*[\s\S]*?\*\//g, '')
}

function escapeRegExp(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}

function extractDeclarations(source: string, sourceName: string): Declaration[] {
  const declarations = [...stripComments(source).matchAll(/(?=(?:^|[;{])\s*(--[^\s:;{}]+)\s*:\s*([^;{}]+);)/g)]

  return declarations.map((match) => {
    const name = match[1]
    if (!CUSTOM_PROPERTY_NAME_PATTERN.test(name)) {
      throw new Error(`[theme-contract] ${sourceName} declares invalid custom property ${name}`)
    }

    return {
      name,
      value: match[2].trim(),
      source: sourceName
    }
  })
}

function extractModeDeclarations(source: string, sourceName: string, selector: ':root' | '.dark'): Declaration[] {
  const declarations: Declaration[] = []
  const blockPattern = new RegExp(`${escapeRegExp(selector)}\\s*\\{([\\s\\S]*?)\\}`, 'g')

  for (const match of stripComments(source).matchAll(blockPattern)) {
    declarations.push(...extractDeclarations(match[1], sourceName))
  }

View on GitHub (pinned to 726446b54c)

Solutions

  1. Rename the custom property to use kebab-case matching ^--[a-z0-9-]+$: only lowercase letters, digits, and hyphens after the leading --.
  2. If the property came from an external source, adapt it to the contract naming convention when bringing it into the theme system.
  3. Run the validator to confirm: npx tsx packages/ui/scripts/validate-theme-contract.ts.

Example fix

/* before */
--primary_Color: #3b82f6;
--MyBackground: #ffffff;

/* after — kebab-case only */
--primary-color: #3b82f6;
--my-background: #ffffff;
Defensive patterns

Strategy: validation

Validate before calling

// Validate custom property names in CSS before committing
import { readFileSync } from 'node:fs'

const VALID_NAME = /^--[a-z0-9-]+$/

function checkPropertyNames(cssPath: string): void {
  const source = readFileSync(cssPath, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
  const decls = [...source.matchAll(/(?=(?:^|[;{])\s*(--[^\s:;{}]+)\s*:)/g)]
  for (const match of decls) {
    if (!VALID_NAME.test(match[1])) {
      throw new Error(`${cssPath}: invalid property name ${match[1]} — use kebab-case`)
    }
  }
}

Prevention

When it happens

Trigger: Any CSS file processed by extractDeclarations contains a declaration like --myVar: red; (underscore), --My-Color: red; (uppercase), --color!: red; (special char), or ---triple: red; (extra dash). The regex extracts all custom property declarations and validates each name against CUSTOM_PROPERTY_NAME_PATTERN.

Common situations: A developer adds a CSS custom property using camelCase or snake_case instead of kebab-case. Copy-pasting CSS from an external library that uses a different naming convention. A typo introducing an invalid character. Mixing CSS naming conventions from different projects.

Related errors


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/40650df08ba1f576. Report an issue: GitHub.