aboutsummaryrefslogtreecommitdiff
path: root/languages/typescript/claude/hooks
diff options
context:
space:
mode:
Diffstat (limited to 'languages/typescript/claude/hooks')
-rwxr-xr-xlanguages/typescript/claude/hooks/validate-typescript.sh94
1 files changed, 94 insertions, 0 deletions
diff --git a/languages/typescript/claude/hooks/validate-typescript.sh b/languages/typescript/claude/hooks/validate-typescript.sh
new file mode 100755
index 0000000..b76f1df
--- /dev/null
+++ b/languages/typescript/claude/hooks/validate-typescript.sh
@@ -0,0 +1,94 @@
+#!/usr/bin/env bash
+# Validate TypeScript/JavaScript files after Edit/Write/MultiEdit.
+# PostToolUse hook: receives tool-call JSON on stdin.
+#
+# On success: exit 0 silent.
+# On failure: emit JSON with hookSpecificOutput.additionalContext so Claude
+# sees a structured error in its context, THEN exit 2 to block the tool
+# pipeline. stderr still echoes the error for terminal visibility.
+#
+# Gate: parseability. A file that doesn't parse is never worth passing on.
+# Full type checking is deliberately NOT enforced here — it needs the whole
+# project graph and its dependencies resolved, which is a build-scale
+# operation, not a per-keystroke one. A type error that parses cleanly passes
+# this hook; `make typecheck` / `tsc --noEmit` over the project owns it.
+#
+# Formatting (prettier) is also out: a project picks its own style, so blocking
+# on an unconfigured default would impose a contested choice.
+#
+# Two checkers, because one tool can't do both jobs:
+#
+# .js/.jsx/.mjs/.cjs → `node --check`, a straight parse.
+# .ts/.tsx/.mts/.cts → `tsc`, filtered to syntax-category diagnostics.
+#
+# `node --check` must NOT be used on TypeScript. It ignores
+# --experimental-strip-types, so it is wrong in *both* directions: it rejects
+# valid TS (an `interface` declaration reads as a syntax error) and accepts
+# broken TS (a genuinely unparseable file exits 0). Measured on node v26.4.0,
+# 2026-07-23. tsc is the only correct parser for these extensions.
+#
+# The tsc call is filtered to TS1xxx codes, which is TypeScript's syntactic
+# diagnostic range; TS2xxx and up are semantic (type) errors and are out of
+# scope by the paragraph above. Without the filter this hook would block every
+# unresolved import in a file whose dependencies aren't installed yet.
+
+set -u
+
+# Emit a JSON failure payload and exit 2. Arguments:
+# $1 — short failure type (e.g. "TYPESCRIPT SYNTAX ERROR")
+# $2 — file path
+# $3 — tool output (error body)
+fail_json() {
+ local ctx
+ ctx="$(printf '%s: %s\n\n%s\n\nFix before proceeding.' "$1" "$2" "$3" \
+ | jq -Rs .)"
+ cat <<EOF
+{"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": $ctx}}
+EOF
+ printf '%s: %s\n%s\n' "$1" "$2" "$3" >&2
+ exit 2
+}
+
+f="$(jq -r '.tool_input.file_path // .tool_response.filePath // empty')"
+[ -z "$f" ] && exit 0
+[ -f "$f" ] || exit 0
+
+# Classify by extension. Match on the basename, not the full path — a parent
+# dir can carry a dot (e.g. my.project/) and confuse a path-wide match.
+kind=""
+base="${f##*/}"
+case "$base" in
+ *.ts | *.tsx | *.mts | *.cts) kind="ts" ;;
+ *.js | *.jsx | *.mjs | *.cjs) kind="js" ;;
+ *) exit 0 ;;
+esac
+
+if [ "$kind" = "js" ]; then
+ command -v node >/dev/null 2>&1 || exit 0
+ if ! out="$(node --check "$f" 2>&1)"; then
+ fail_json "JAVASCRIPT SYNTAX ERROR" "$f" "$out"
+ fi
+ exit 0
+fi
+
+# TypeScript. Prefer a project-local tsc so the project's own version decides,
+# falling back to one on PATH.
+tsc_bin=""
+if [ -x "./node_modules/.bin/tsc" ]; then
+ tsc_bin="./node_modules/.bin/tsc"
+elif command -v tsc >/dev/null 2>&1; then
+ tsc_bin="tsc"
+else
+ exit 0 # no TypeScript compiler available — don't block the edit
+fi
+
+# --moduleDetection force so a file with no import/export still parses as a
+# module rather than tripping global-scope collisions against lib types.
+out="$("$tsc_bin" --noEmit --skipLibCheck --target es2022 --moduleDetection force "$f" 2>&1 || true)"
+syntax_errors="$(printf '%s\n' "$out" | grep -E 'error TS1[0-9]{3}:' || true)"
+
+if [ -n "$syntax_errors" ]; then
+ fail_json "TYPESCRIPT SYNTAX ERROR" "$f" "$syntax_errors"
+fi
+
+exit 0