#!/usr/bin/env bash # purpose: enforce the theme contract — every var(--token) a consumer's CSS uses must be defined in the synced theme (or the consumer's own CSS), and a theme's colors must live only in :root | usage: bash scripts/theme-contract.sh --theme | bash scripts/theme-contract.sh --consumer | inputs: css files | outputs: missing-token list + exit 0/1 set -euo pipefail MODE="${1:-}" case "$MODE" in --theme) THEME_CSS="${2:?usage: theme-contract.sh --theme }" ;; --consumer) SRC_DIR="${2:?usage: theme-contract.sh --consumer }" THEME_CSS="${3:?usage: theme-contract.sh --consumer }" ;; *) echo "usage: theme-contract.sh --theme | --consumer " >&2 exit 2 ;; esac [ -f "$THEME_CSS" ] || { echo "theme-contract: theme.css not found: $THEME_CSS" >&2; exit 1; } # Tokens DEFINED in the theme: lines like " --token-name: value;" (any scope). # Note: pipelines end in `|| true` — grep exits 1 on no match and set -e # would otherwise abort the script on the first token-less file. theme_tokens() { sed -E 's|/\*.*\*/||g' "$THEME_CSS" \ | grep -oE '[[:space:]]*--[A-Za-z0-9_-]+[[:space:]]*:' \ | sed -E 's/^[[:space:]]*(--[A-Za-z0-9_-]+)[[:space:]]*:$/\1/' \ | sort -u || true } extract_defined_tokens() { # $1 = css file -> defined --token names sed -E 's|/\*.*\*/||g' "$1" \ | grep -oE '[[:space:]]*--[A-Za-z0-9_-]+[[:space:]]*:' \ | sed -E 's/^[[:space:]]*(--[A-Za-z0-9_-]+)[[:space:]]*:$/\1/' \ || true } extract_used_vars() { # $1 = css file -> used var(--token) names sed -E 's|/\*.*\*/||g' "$1" \ | grep -oE 'var\(--[A-Za-z0-9_-]+' \ | sed -E 's/^var\((--[A-Za-z0-9_-]+)$/\1/' \ || true } if [ "$MODE" = "--theme" ]; then # Theme-side rule: no hardcoded colors outside the :root block. OUTSIDE_COLORS=$(awk '/:root/{inroot=1} /^\}/{inroot=0} !inroot' "$THEME_CSS" \ | grep -cE '#[0-9a-fA-F]{3,8}|hsl\(|rgb\(' || true) if [ "$OUTSIDE_COLORS" -ne "0" ]; then echo "theme-contract FAIL: $OUTSIDE_COLORS hardcoded color(s) outside :root in $THEME_CSS (tokens only, then var())" >&2 exit 1 fi echo "theme-contract: theme OK — token count: $(theme_tokens | wc -l | tr -d ' '), all colors inside :root" exit 0 fi # Consumer-side rule: every var(--x) in consumer CSS resolves to a token # defined in the theme OR in the consumer's own CSS. [ -d "$SRC_DIR" ] || { echo "theme-contract: src dir not found: $SRC_DIR" >&2; exit 1; } THEME_TOKENS=$(theme_tokens) DEFINED="$THEME_TOKENS" USED_FILE=$(mktemp) trap 'rm -f "$USED_FILE"' EXIT # Gather consumer-defined tokens + used vars from every css file under SRC_DIR, # skipping node_modules and the synced theme copy itself (it IS the theme). while IFS= read -r css; do case "$css" in */node_modules/*|*/src/styles/theme/*) continue ;; esac DEFINED="$DEFINED $(extract_defined_tokens "$css")" extract_used_vars "$css" >> "$USED_FILE" done < <(find "$SRC_DIR" -name '*.css' -type f) sort -u "$USED_FILE" > "$USED_FILE.sorted" mv "$USED_FILE.sorted" "$USED_FILE" MISSING=0 while IFS= read -r tok; do [ -z "$tok" ] && continue if ! grep -qxF -- "$tok" <<< "$DEFINED" 2>/dev/null; then echo "theme-contract: undefined token used: $tok" >&2 MISSING=1 fi done < "$USED_FILE" if [ "$MISSING" -ne "0" ]; then echo "theme-contract FAIL: consumer CSS references tokens not defined in theme.css or its own CSS" >&2 exit 1 fi echo "theme-contract: consumer OK — $(wc -l < "$USED_FILE" | tr -d ' ') unique var() tokens all resolve" exit 0