Skip to content

Check_frontend

FieldValue
TypeSkill Resource
Source~/.copilot/skills/frontend/scripts/check_frontend.sh
DescriptionNot specified

Source Content

#!/usr/bin/env bash
# House checks for senior-frontend / senior-fullstack TypeScript+React work.
#
# Resolution order for linting (each degrades, never crashes, if absent):
# 1. oxlint (repo-local node_modules/.bin/oxlint, else `npx oxlint` if
# reachable) — 50-100x faster than eslint. Runs with the bundled
# templates/oxlint/.oxlintrc.json, which adds one custom JS plugin rule
# (no-unnecessary-react-import) on top of oxlint's defaults, plus
# typescript/no-explicit-any as a hard error. Adds --type-aware
# automatically when `oxlint-tsgolint` is installed in the target repo
# (experimental/opt-in upstream — see oxc.rs/docs/guide/usage/linter).
# 2. eslint (repo-local node_modules/.bin/eslint) — only if oxlint isn't
# available at all.
# 3. tsc --noEmit (repo-local node_modules/.bin/tsc) — always attempted;
# oxlint/eslint don't replace real type-checking.
#
# PLUS grep-based house-rule checks needing no external tool, used as a
# last-resort fallback when NEITHER oxlint nor eslint is installed (oxlint's
# custom plugin and tsc-driven typing already cover these more accurately):
# (a) no explicit `any` in .ts/.tsx — HARD FAILURE if found.
# (b) unnecessary `import React from 'react'` — HARD FAILURE if the import
# exists and `React.` never appears elsewhere in the file.
#
# ALWAYS run regardless of tooling availability:
# (c) heuristic for the "derived state synced via useEffect" anti-pattern —
# fuzzy grep, WARNING only, never a hard fail.
# (d) tsconfig.json has `"strict": true` (and doesn't override
# `noImplicitAny: false`) — HARD FAILURE. This is the single setting
# that prevents "no types at all" from compiling silently.
#
# Usage: check_frontend.sh [TARGET_REPO_PATH]
# TARGET_REPO_PATH defaults to "."
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OXLINT_CONFIG="$SCRIPT_DIR/../templates/oxlint/.oxlintrc.json"
TARGET="${1:-.}"
if [[ ! -d "$TARGET" ]]; then
echo "Usage: $0 [TARGET_REPO_PATH]" >&2
echo "error: '$TARGET' is not a directory" >&2
exit 2
fi
fail=0
warn() { printf '⚠️ %s\n' "$1"; }
error() { printf '❌ %s\n' "$1"; fail=1; }
ok() { printf '✅ %s\n' "$1"; }
oxlint_bin=""
if [[ -x "$TARGET/node_modules/.bin/oxlint" ]]; then
oxlint_bin="$TARGET/node_modules/.bin/oxlint"
elif command -v npx >/dev/null 2>&1 && npx --no-install oxlint --version >/dev/null 2>&1; then
oxlint_bin="npx --no-install oxlint"
fi
echo "== oxlint =="
oxlint_ran=0
if [[ -n "$oxlint_bin" ]]; then
oxlint_ran=1
type_aware_flag=()
if [[ -d "$TARGET/node_modules/oxlint-tsgolint" ]]; then
type_aware_flag=(--type-aware)
ok "oxlint-tsgolint detected — running with --type-aware"
fi
if (cd "$TARGET" && $oxlint_bin -c "$OXLINT_CONFIG" --ignore-pattern 'node_modules' --ignore-pattern 'dist' --ignore-pattern 'build' --ignore-pattern '.next' "${type_aware_flag[@]}" .); then
ok "oxlint clean"
else
error "oxlint reported errors"
fi
else
warn "oxlint not found (repo-local node_modules/.bin/oxlint or npx oxlint) — falling back to eslint"
echo "== eslint =="
if [[ -x "$TARGET/node_modules/.bin/eslint" ]]; then
if (cd "$TARGET" && node_modules/.bin/eslint .); then
ok "eslint clean"
else
error "eslint reported errors"
fi
else
warn "eslint not found at node_modules/.bin/eslint either — skipping all lint checks"
fi
fi
echo
echo "== tsc --noEmit =="
if [[ -x "$TARGET/node_modules/.bin/tsc" ]]; then
if (cd "$TARGET" && node_modules/.bin/tsc --noEmit); then
ok "tsc --noEmit clean"
else
error "tsc --noEmit reported type errors"
fi
else
warn "tsc not found at node_modules/.bin/tsc — skipping"
fi
# Collect candidate .ts/.tsx files once, excluding common build/output dirs.
mapfile -t TS_FILES < <(find "$TARGET" -type f \( -name '*.ts' -o -name '*.tsx' \) \
-not -path '*/node_modules/*' \
-not -path '*/dist/*' \
-not -path '*/build/*' \
-not -path '*/.next/*' \
2>/dev/null)
if [[ "$oxlint_ran" -eq 0 ]]; then
echo
echo "== house rule (fallback, no oxlint/eslint installed): no explicit 'any' =="
if [[ "${#TS_FILES[@]}" -gt 0 ]]; then
any_hits=$(grep -nE ':\s*any\b|<any>|as\s+any\b' "${TS_FILES[@]}" 2>/dev/null || true)
if [[ -n "$any_hits" ]]; then
error "found explicit 'any' usage — replace with a real type or 'unknown':"
echo "$any_hits"
else
ok "no explicit 'any' found in ${#TS_FILES[@]} .ts/.tsx files"
fi
else
ok "no .ts/.tsx files found — skipping"
fi
echo
echo "== house rule (fallback, no oxlint/eslint installed): no unnecessary 'import React from react' =="
if [[ "${#TS_FILES[@]}" -gt 0 ]]; then
bad_react_import=0
for f in "${TS_FILES[@]}"; do
import_line=$(grep -nE "^\s*import\s+(\*\s+as\s+)?React\s*(,|from)" "$f" || true)
[[ -z "$import_line" ]] && continue
uses_namespace=$(grep -nE '\bReact\.[A-Za-z]' "$f" | grep -vE '^\s*[0-9]+:\s*import\s' || true)
if [[ -z "$uses_namespace" ]]; then
echo "$f: imports React by default but never references React.* — remove it (React 19 automatic JSX runtime doesn't need it)"
echo " $import_line"
bad_react_import=1
fi
done
if [[ "$bad_react_import" -eq 1 ]]; then
fail=1
else
ok "no unnecessary default React import found in ${#TS_FILES[@]} .ts/.tsx files"
fi
else
ok "no .ts/.tsx files found — skipping"
fi
fi
echo
echo "== heuristic: derived state synced via useEffect (fuzzy, warning only) =="
if [[ "${#TS_FILES[@]}" -gt 0 ]]; then
# Look for useEffect blocks whose body is essentially a single setState call:
# useEffect(() => { setX(...); }, [...]) all on one line, or a tight 3-line block.
effect_hits=$(grep -nE 'useEffect\(\(\)\s*=>\s*\{\s*set[A-Z][a-zA-Z0-9_]*\(' "${TS_FILES[@]}" 2>/dev/null || true)
if [[ -n "$effect_hits" ]]; then
warn "possible derived-state-via-useEffect anti-pattern (heuristic, may be a false positive) — consider deriving during render instead:"
echo "$effect_hits"
else
ok "no obvious single-setState useEffect pattern found"
fi
else
ok "no .ts/.tsx files found — skipping"
fi
echo
echo "== house rule: tsconfig strict mode =="
tsconfig="$TARGET/tsconfig.json"
if [[ -f "$tsconfig" ]]; then
if grep -qE '"strict"\s*:\s*true' "$tsconfig"; then
if grep -qE '"noImplicitAny"\s*:\s*false' "$tsconfig"; then
error "tsconfig.json sets strict:true but overrides noImplicitAny:false — this reopens the exact hole strict mode closes"
else
ok "tsconfig.json has strict: true"
fi
else
if grep -q '"extends"' "$tsconfig"; then
warn "tsconfig.json has no local 'strict: true' but extends a base config — confirm the base sets it (this check doesn't follow extends)"
else
error "tsconfig.json has no \"strict\": true — implicit-any and missing types will compile silently. Add it."
fi
fi
else
warn "no tsconfig.json found at $TARGET — skipping strict-mode check"
fi
echo
if [[ "$fail" -eq 0 ]]; then
echo "All checks passed."
else
echo "One or more checks failed — see ❌ lines above."
fi
exit "$fail"