Files
dss/scripts/validate-theme.py
DSS 08ce228df1
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
feat: Add DSS infrastructure, remove legacy admin-ui code
- Remove legacy admin-ui/js/ vanilla JS components
- Add .dss/ directory with core tokens, skins, themes
- Add Storybook configuration and generated stories
- Add DSS management scripts (dss-services, dss-init, dss-setup, dss-reset)
- Add MCP command definitions for DSS plugin
- Add Figma sync architecture and scripts
- Update pre-commit hooks with documentation validation
- Fix JSON trailing commas in skin files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-10 22:15:11 -03:00

279 lines
8.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""
DSS Theme Validation Script
Validates that themes only override tokens defined in the skin contract.
Usage: python3 scripts/validate-theme.py [--theme THEME_NAME] [--skin SKIN_NAME]
Defaults to validating all themes against the skin contract.
"""
import sys
import os
import json
from pathlib import Path
from typing import Dict, List, Set, Tuple
DSS_ROOT = Path(__file__).parent.parent
DSS_DATA = DSS_ROOT / ".dss"
def load_json(path: Path) -> dict:
"""Load JSON file"""
if not path.exists():
return {}
with open(path) as f:
return json.load(f)
def get_contract_tokens(contract: dict) -> Dict[str, Set[str]]:
"""Extract required token names from contract by category"""
required = contract.get("required_tokens", {})
result = {}
for category, data in required.items():
if isinstance(data, dict) and "required" in data:
result[category] = set(data["required"])
return result
def get_theme_tokens(theme: dict) -> Dict[str, Set[str]]:
"""Extract token names from theme by category"""
result = {}
for key, value in theme.items():
if key.startswith("_"):
continue
if isinstance(value, dict):
# Check if it's a token (has 'value' key) or a category
if "value" in value:
# Single token at root level
if "root" not in result:
result["root"] = set()
result["root"].add(key)
else:
# Category with nested tokens
tokens = set()
for token_name, token_data in value.items():
if isinstance(token_data, dict):
tokens.add(token_name)
if tokens:
result[key] = tokens
return result
def get_skin_tokens(skin: dict) -> Dict[str, Set[str]]:
"""Extract token names from skin by category"""
return get_theme_tokens(skin) # Same structure
def validate_theme(
theme_path: Path,
contract_path: Path,
skin_path: Path = None
) -> Tuple[bool, List[str], List[str]]:
"""
Validate a theme against the skin contract.
Returns: (is_valid, errors, warnings)
"""
errors = []
warnings = []
contract = load_json(contract_path)
theme = load_json(theme_path)
if not contract:
errors.append(f"Contract not found: {contract_path}")
return False, errors, warnings
if not theme:
errors.append(f"Theme not found: {theme_path}")
return False, errors, warnings
contract_tokens = get_contract_tokens(contract)
theme_tokens = get_theme_tokens(theme)
# Load skin if provided for additional context
skin_tokens = {}
if skin_path and skin_path.exists():
skin = load_json(skin_path)
skin_tokens = get_skin_tokens(skin)
# Check each category in the theme
for category, tokens in theme_tokens.items():
# Handle dark mode variants
base_category = category.replace("-dark", "")
if base_category not in contract_tokens:
# Category not in contract - check if skin provides it
if base_category in skin_tokens:
warnings.append(
f"Category '{category}' not in contract but exists in skin. "
f"Consider adding to contract for stability."
)
else:
errors.append(
f"Category '{category}' is not defined in the skin contract. "
f"Theme should only override contract-defined tokens."
)
continue
# Check each token in the category
contract_category = contract_tokens[base_category]
for token in tokens:
if token not in contract_category:
# Token not in contract
if skin_tokens.get(base_category) and token in skin_tokens[base_category]:
warnings.append(
f"Token '{category}.{token}' exists in skin but not in contract. "
f"May break on skin updates."
)
else:
errors.append(
f"Token '{category}.{token}' is not in the skin contract. "
f"Valid tokens: {sorted(contract_category)}"
)
is_valid = len(errors) == 0
return is_valid, errors, warnings
def validate_skin(
skin_path: Path,
contract_path: Path
) -> Tuple[bool, List[str], List[str]]:
"""
Validate that a skin provides all required contract tokens.
Returns: (is_valid, errors, warnings)
"""
errors = []
warnings = []
contract = load_json(contract_path)
skin = load_json(skin_path)
if not contract:
errors.append(f"Contract not found: {contract_path}")
return False, errors, warnings
if not skin:
errors.append(f"Skin not found: {skin_path}")
return False, errors, warnings
contract_tokens = get_contract_tokens(contract)
skin_tokens = get_skin_tokens(skin)
# Check all required categories exist
for category, required in contract_tokens.items():
if category not in skin_tokens:
errors.append(
f"Skin missing required category: '{category}'. "
f"Required tokens: {sorted(required)}"
)
continue
# Check all required tokens exist
skin_category = skin_tokens[category]
missing = required - skin_category
if missing:
errors.append(
f"Skin missing required tokens in '{category}': {sorted(missing)}"
)
# Note extra tokens (not an error, just info)
extra = skin_category - required
if extra:
warnings.append(
f"Skin has extra tokens in '{category}' (OK): {sorted(extra)}"
)
is_valid = len(errors) == 0
return is_valid, errors, warnings
def main():
import argparse
parser = argparse.ArgumentParser(description="Validate DSS themes and skins")
parser.add_argument("--theme", help="Theme name to validate (default: all)")
parser.add_argument("--skin", help="Skin name to validate (default: shadcn)")
parser.add_argument("--validate-skin", action="store_true", help="Validate skin against contract")
parser.add_argument("--quiet", "-q", action="store_true", help="Only show errors")
args = parser.parse_args()
contract_path = DSS_DATA / "schema" / "skin-contract.json"
print("=" * 60)
print("DSS THEME/SKIN VALIDATION")
print("=" * 60)
all_valid = True
# Validate skin if requested
if args.validate_skin or args.skin:
skin_name = args.skin or "shadcn"
skin_path = DSS_DATA / "skins" / skin_name / "tokens.json"
print(f"\n[SKIN] Validating: {skin_name}")
print("-" * 40)
is_valid, errors, warnings = validate_skin(skin_path, contract_path)
if errors:
all_valid = False
for err in errors:
print(f" [ERROR] {err}")
if warnings and not args.quiet:
for warn in warnings:
print(f" [WARN] {warn}")
if is_valid:
print(f" [OK] Skin '{skin_name}' provides all contract tokens")
# Validate themes
themes_dir = DSS_DATA / "themes"
skin_path = DSS_DATA / "skins" / (args.skin or "shadcn") / "tokens.json"
if args.theme:
themes = [args.theme]
else:
themes = [
p.stem for p in themes_dir.glob("*.json")
if not p.stem.startswith("_")
] if themes_dir.exists() else []
for theme_name in themes:
theme_path = themes_dir / f"{theme_name}.json"
print(f"\n[THEME] Validating: {theme_name}")
print("-" * 40)
is_valid, errors, warnings = validate_theme(
theme_path, contract_path, skin_path
)
if errors:
all_valid = False
for err in errors:
print(f" [ERROR] {err}")
if warnings and not args.quiet:
for warn in warnings:
print(f" [WARN] {warn}")
if is_valid:
print(f" [OK] Theme '{theme_name}' is valid")
print("\n" + "=" * 60)
if all_valid:
print("[OK] All validations passed!")
sys.exit(0)
else:
print("[FAIL] Validation errors found")
sys.exit(1)
if __name__ == "__main__":
main()