feat: Add DSS infrastructure, remove legacy admin-ui code
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
- 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>
This commit is contained in:
278
scripts/validate-theme.py
Executable file
278
scripts/validate-theme.py
Executable file
@@ -0,0 +1,278 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user