#!/usr/bin/env bash # lint-package-css.sh — Enforce extension CSS prefix convention # # Rule: The FIRST class selector in every rule must start with .ext-{slug}. # Subsequent classes in compound/descendant selectors are allowed (they're # scoped under the extension's namespace). Kernel .sw-* classes are always # allowed. CodeMirror .cm-* classes are allowed as descendants. # # Exemptions: :root, @keyframes blocks, @font-face blocks, @media/@supports # wrappers, CSS comments. # # Usage: bash scripts/lint-package-css.sh [packages-dir] # Exit 0 = all clean, Exit 1 = violations found. set -euo pipefail PKG_DIR="${1:-packages}" VIOLATIONS=0 for css in "$PKG_DIR"/*/css/main.css; do [ -f "$css" ] || continue # Derive slug from directory: {pkg_dir}/{slug}/css/main.css slug="$(basename "$(dirname "$(dirname "$css")")")" prefix="ext-${slug}" in_keyframes=0 in_fontface=0 in_comment=0 brace_depth=0 lineno=0 while IFS= read -r line; do lineno=$((lineno + 1)) # Track block comments if (( in_comment )); then [[ "$line" == *"*/"* ]] && in_comment=0 continue fi if [[ "$line" == *"/*"* ]] && [[ "$line" != *"*/"* ]]; then in_comment=1 continue fi # Skip single-line comments trimmed="${line#"${line%%[![:space:]]*}"}" [[ "$trimmed" == "" ]] && continue [[ "$trimmed" == /\** ]] && continue # Track @keyframes blocks if [[ "$trimmed" =~ ^@keyframes ]]; then in_keyframes=1 continue fi if (( in_keyframes )); then [[ "$trimmed" == "}" ]] && in_keyframes=0 continue fi # Skip @font-face if [[ "$trimmed" =~ ^@font-face ]]; then in_fontface=1 continue fi if (( in_fontface )); then [[ "$trimmed" == *"}"* ]] && in_fontface=0 continue fi # Skip @-rule wrappers [[ "$trimmed" =~ ^@ ]] && continue # Skip :root selectors [[ "$trimmed" =~ ^:root ]] && continue # Skip closing braces [[ "$trimmed" == "}" ]] && continue # Skip property declarations (contain : but not {) if [[ "$trimmed" == *":"* ]] && [[ "$trimmed" != *"{"* ]]; then continue fi # Selector line — check if it contains a class if [[ "$trimmed" == *"."* ]]; then # Split comma-separated selectors and check each one # We process the whole line as potentially multiple selectors IFS=',' read -ra selectors <<< "$trimmed" for sel in "${selectors[@]}"; do # Trim whitespace sel="${sel#"${sel%%[![:space:]]*}"}" sel="${sel%"${sel##*[![:space:]]}"}" # Remove trailing { if present sel="${sel%\{}" sel="${sel%"${sel##*[![:space:]]}"}" # Skip if empty [[ -z "$sel" ]] && continue # Find the FIRST class selector in this selector if [[ "$sel" =~ \.([a-zA-Z_][a-zA-Z0-9_-]*) ]]; then first_cls="${BASH_REMATCH[1]}" # Allow kernel .sw-* as first class [[ "$first_cls" == sw-* ]] && continue # Allow [data-ext=...] scoped selectors [[ "$sel" =~ ^\[data-ext ]] && continue # Must start with ext-{slug} if [[ "$first_cls" != ${prefix}* ]]; then echo "VIOLATION: $css:$lineno — .${first_cls} (expected .${prefix}-*)" VIOLATIONS=$((VIOLATIONS + 1)) fi fi done fi done < "$css" done if (( VIOLATIONS > 0 )); then echo "" echo "Found $VIOLATIONS violation(s). First class selector in each rule must start with .ext-{slug}-" exit 1 else echo "All package CSS files pass prefix enforcement." exit 0 fi