All checks were successful
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
130 lines
3.5 KiB
Bash
Executable File
130 lines
3.5 KiB
Bash
Executable File
#!/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
|