From 828d46722660f0e662b8ec7400d69975fc1b12cd Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Thu, 5 Mar 2026 19:20:27 +0100 Subject: [PATCH] chore: add bb check-dev script for pre-commit server verification Babashka script that checks all ui-dev tmux panes for compile errors, verifies hiccup serves content, ensures squint .mjs files aren't truncated, and confirms replicant JS is compiled. Replaces the manual tmux capture-pane + curl checks documented in AGENTS.md. --- AGENTS.md | 39 ++------- bb.edn | 4 + dev/hiccup/src/dev/hiccup.clj | 36 ++++++++- dev/replicant/src/dev/replicant.cljs | 51 ++++++++++-- dev/squint/src/dev/squint.cljs | 51 +++++++++++- scripts/check-dev.bb | 117 +++++++++++++++++++++++++++ 6 files changed, 253 insertions(+), 45 deletions(-) create mode 100755 scripts/check-dev.bb diff --git a/AGENTS.md b/AGENTS.md index df964ba..e8f677b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -205,46 +205,19 @@ If a dev server needs restarting, **tell the user** — don't do it yourself. ### 7. Check running dev servers before committing — CRITICAL -A tmux session `ui-dev` runs all three dev servers (`bb dev-all`). **Always check every pane for compile errors before committing:** +A tmux session `ui-dev` runs all three dev servers (`bb dev-all`). **Always run the check script before committing:** ```sh -# List panes, then check each for errors -tmux list-panes -t ui-dev -F "#{pane_index}: #{pane_current_command}" -for i in $(tmux list-panes -t ui-dev -F "#{pane_index}"); do - echo "=== pane $i ===" - tmux capture-pane -t "ui-dev:bash.$i" -p -S -30 | grep -v '^$' | tail -10 -done +bb check-dev ``` -Look for: -- **shadow-cljs** (Replicant): `Build failure`, warnings, or `CompilerException` -- **Vite/Squint**: `ERROR`, `SyntaxError`, or failed imports -- **Hiccup** (Babashka): stack traces or `Exception` +This checks all tmux panes for compile errors (shadow-cljs failures, Vite/Squint errors, Babashka exceptions), verifies the hiccup server responds with content, ensures all squint `.mjs` files have content (catches silent empty-file bugs), and confirms replicant JS is compiled. -Do **not** commit if any pane shows errors. Fix them first. - -### 8. Verify pages load in browser before committing — CRITICAL - -Terminal compile checks alone are **not enough**. Squint can produce empty `.mjs` files silently (no errors in the terminal). Use the `fetch` tool to verify each dev server actually serves a working page: +Do **not** commit if `bb check-dev` exits non-zero. Fix errors first. +If a compiled squint file is empty (1 line = just the import), touch the source to trigger rewatch: ```sh -# Check hiccup (server-rendered — look for actual HTML content) -curl -s http://localhost:4003 | grep -c 'sidebar-layout' - -# Check squint compiled output is not empty (must be >1 line) -wc -l dev/squint/.compiled/dev/squint.mjs -wc -l dev/squint/.compiled/ui/*.mjs | sort -n | head -5 -# Any file with only 1 line is broken — recompile it: -# touch src/ui/.cljc - -# Check replicant compiled JS exists -ls -la dev/replicant/public/js/cljs-runtime/ui.sidebar.js -``` - -If any compiled file is suspiciously small (1 line = just the import), touch the source file to trigger rewatch, or manually compile: -```sh -cd dev/squint && npx squint compile ../../src/ui/.cljc -cd dev/squint && npx squint compile src/dev/squint.cljs +touch src/ui/.cljc ``` ## Theme System diff --git a/bb.edn b/bb.edn index e73a5fb..9fbfb46 100644 --- a/bb.edn +++ b/bb.edn @@ -102,6 +102,10 @@ (println " Test page: dev/index.html") (deref (promise)))} + check-dev + {:doc "Check all ui-dev tmux panes for errors and verify dev servers" + :task (load-file "scripts/check-dev.bb")} + dev-all {:doc "Start all dev servers in tmux panes (bb dev-all [BASE_PORT], default 3000)" :depends [build-theme ensure-npm] diff --git a/dev/hiccup/src/dev/hiccup.clj b/dev/hiccup/src/dev/hiccup.clj index 6e9a661..97473f3 100644 --- a/dev/hiccup/src/dev/hiccup.clj +++ b/dev/hiccup/src/dev/hiccup.clj @@ -72,9 +72,10 @@ ;; ── Helpers ───────────────────────────────────────────────────────── (defn section [title & children] - [:section {:style "margin-bottom: 2.5rem;"} - [:h3 {:style "color: var(--fg-1); margin-bottom: 1rem; border-bottom: var(--border-0); padding-bottom: 0.5rem;"} title] - (into [:div {:style "display: flex; flex-direction: column; gap: 1rem;"}] children)]) + (let [id (str/lower-case title)] + [:section {:id id :style "margin-bottom: 2.5rem;"} + [:h3 {:style "color: var(--fg-1); margin-bottom: 1rem; border-bottom: var(--border-0); padding-bottom: 0.5rem;"} title] + (into [:div {:style "display: flex; flex-direction: column; gap: 1rem;"}] children)])) ;; ── Component Demos ───────────────────────────────────────────────── @@ -370,6 +371,28 @@ ;; ── Navigation Data ───────────────────────────────────────────────── +(def component-nav + [{:title "General" + :items [{:label "Button" :anchor "button"} + {:label "Badge" :anchor "badge"} + {:label "Card" :anchor "card"}]} + {:title "Forms" + :items [{:label "Form" :anchor "form"} + {:label "Switch" :anchor "switch"}]} + {:title "Data Display" + :items [{:label "Table" :anchor "table"} + {:label "Accordion" :anchor "accordion"} + {:label "Progress" :anchor "progress"}]} + {:title "Feedback" + :items [{:label "Alert" :anchor "alert"} + {:label "Dialog" :anchor "dialog"} + {:label "Spinner" :anchor "spinner"} + {:label "Skeleton" :anchor "skeleton"} + {:label "Tooltip" :anchor "tooltip"}]} + {:title "Navigation" + :items [{:label "Breadcrumb" :anchor "breadcrumb"} + {:label "Pagination" :anchor "pagination"}]}]) + (def nav-items [{:id :components :label "Components" :icon-name :package :href "/"} {:id :icons :label "Icons" :icon-name :image :href "/icons"} @@ -402,6 +425,13 @@ {:href href :icon-name icon-name :active (= id active-page)} label)))) (sidebar/sidebar-separator) + (sidebar/sidebar-group {:label "Components"} + (for [{:keys [title items]} component-nav] + (sidebar/sidebar-collapsible {:title title :open true} + (apply sidebar/sidebar-menu {} + (for [{:keys [label anchor]} items] + (sidebar/sidebar-menu-item {:href (str "/#" anchor)} label)))))) + (sidebar/sidebar-separator) (sidebar/sidebar-group {:label "Targets"} (apply sidebar/sidebar-menu {} (for [{:keys [label port active]} (make-targets own-port)] diff --git a/dev/replicant/src/dev/replicant.cljs b/dev/replicant/src/dev/replicant.cljs index c3af2e2..d10fb93 100644 --- a/dev/replicant/src/dev/replicant.cljs +++ b/dev/replicant/src/dev/replicant.cljs @@ -1,5 +1,6 @@ (ns dev.replicant - (:require [replicant.dom :as d] + (:require [clojure.string :as str] + [replicant.dom :as d] [ui.button :as button] [ui.alert :as alert] [ui.badge :as badge] @@ -25,10 +26,11 @@ ;; ── Helpers ───────────────────────────────────────────────────────── (defn section [title & children] - [:section {:style {:margin-bottom "2.5rem"}} - [:h3 {:style {:color "var(--fg-1)" :margin-bottom "1rem" - :border-bottom "var(--border-0)" :padding-bottom "0.5rem"}} title] - (into [:div {:style {:display "flex" :flex-direction "column" :gap "1rem"}}] children)]) + (let [id (str/lower-case title)] + [:section {:id id :style {:margin-bottom "2.5rem"}} + [:h3 {:style {:color "var(--fg-1)" :margin-bottom "1rem" + :border-bottom "var(--border-0)" :padding-bottom "0.5rem"}} title] + (into [:div {:style {:display "flex" :flex-direction "column" :gap "1rem"}}] children)])) (defn page-header [title subtitle] [:div {:style {:margin-bottom "2rem"}} @@ -326,6 +328,28 @@ ;; ── Navigation ────────────────────────────────────────────────────── +(def component-nav + [{:title "General" + :items [{:label "Button" :anchor "button"} + {:label "Badge" :anchor "badge"} + {:label "Card" :anchor "card"}]} + {:title "Forms" + :items [{:label "Form" :anchor "form"} + {:label "Switch" :anchor "switch"}]} + {:title "Data Display" + :items [{:label "Table" :anchor "table"} + {:label "Accordion" :anchor "accordion"} + {:label "Progress" :anchor "progress"}]} + {:title "Feedback" + :items [{:label "Alert" :anchor "alert"} + {:label "Dialog" :anchor "dialog"} + {:label "Spinner" :anchor "spinner"} + {:label "Skeleton" :anchor "skeleton"} + {:label "Tooltip" :anchor "tooltip"}]} + {:title "Navigation" + :items [{:label "Breadcrumb" :anchor "breadcrumb"} + {:label "Pagination" :anchor "pagination"}]}]) + (def nav-items [{:id :components :label "Components" :icon-name :package} {:id :icons :label "Icons" :icon-name :image} @@ -335,6 +359,16 @@ (fn [_e] (reset! !page page-id))) +(defn navigate-to-section! [anchor] + (fn [_e] + (when (not= @!page :components) + (reset! !page :components)) + (js/setTimeout + (fn [] + (when-let [el (.getElementById js/document anchor)] + (.scrollIntoView el #js {:behavior "smooth" :block "start"}))) + 50))) + (defn toggle-theme! [_e] (let [el (.-documentElement js/document) current (.. el -dataset -theme)] @@ -378,6 +412,13 @@ :on-click (navigate! id)} label)))) (sidebar/sidebar-separator) + (sidebar/sidebar-group {:label "Components"} + (for [{:keys [title items]} component-nav] + (sidebar/sidebar-collapsible {:title title :open true} + (apply sidebar/sidebar-menu {} + (for [{:keys [label anchor]} items] + (sidebar/sidebar-menu-item {:on-click (navigate-to-section! anchor)} label)))))) + (sidebar/sidebar-separator) (sidebar/sidebar-group {:label "Targets"} (apply sidebar/sidebar-menu {} (for [{:keys [label port active]} (make-targets)] diff --git a/dev/squint/src/dev/squint.cljs b/dev/squint/src/dev/squint.cljs index 605444d..0c9c31b 100644 --- a/dev/squint/src/dev/squint.cljs +++ b/dev/squint/src/dev/squint.cljs @@ -42,10 +42,11 @@ (.removeAttribute layout "data-sidebar-open"))) (defn section [title & children] - [:section {:style {"margin-bottom" "2.5rem"}} - [:h3 {:style {"color" "var(--fg-1)" "margin-bottom" "1rem" - "border-bottom" "var(--border-0)" "padding-bottom" "0.5rem"}} title] - (into [:div {:style {"display" "flex" "flex-direction" "column" "gap" "1rem"}}] children)]) + (let [id (.toLowerCase title)] + [:section {:id id :style {"margin-bottom" "2.5rem"}} + [:h3 {:style {"color" "var(--fg-1)" "margin-bottom" "1rem" + "border-bottom" "var(--border-0)" "padding-bottom" "0.5rem"}} title] + (into [:div {:style {"display" "flex" "flex-direction" "column" "gap" "1rem"}}] children)])) (defn page-header [title subtitle] [:div {:style {"margin-bottom" "2rem"}} @@ -353,6 +354,28 @@ ;; ── Navigation ────────────────────────────────────────────────────── +(def component-nav + [{:title "General" + :items [{:label "Button" :anchor "button"} + {:label "Badge" :anchor "badge"} + {:label "Card" :anchor "card"}]} + {:title "Forms" + :items [{:label "Form" :anchor "form"} + {:label "Switch" :anchor "switch"}]} + {:title "Data Display" + :items [{:label "Table" :anchor "table"} + {:label "Accordion" :anchor "accordion"} + {:label "Progress" :anchor "progress"}]} + {:title "Feedback" + :items [{:label "Alert" :anchor "alert"} + {:label "Dialog" :anchor "dialog"} + {:label "Spinner" :anchor "spinner"} + {:label "Skeleton" :anchor "skeleton"} + {:label "Tooltip" :anchor "tooltip"}]} + {:title "Navigation" + :items [{:label "Breadcrumb" :anchor "breadcrumb"} + {:label "Pagination" :anchor "pagination"}]}]) + (def nav-items [{:id "components" :label "Components" :icon-name "package"} {:id "icons" :label "Icons" :icon-name "image"} @@ -363,6 +386,17 @@ (reset! !page page-id) (render!))) +(defn navigate-to-section! [anchor] + (fn [_e] + (when (not= @!page "components") + (reset! !page "components") + (render!)) + (js/setTimeout + (fn [] + (when-let [el (js/document.getElementById anchor)] + (.scrollIntoView el {"behavior" "smooth" "block" "start"}))) + 50))) + ;; ── App Shell ─────────────────────────────────────────────────────── (defn own-port [] @@ -390,6 +424,15 @@ label)) nav-items))) (sidebar/sidebar-separator) + (into (sidebar/sidebar-group {:label "Components"}) + (map (fn [{:keys [title items]}] + (sidebar/sidebar-collapsible {:title title :open true} + (into (sidebar/sidebar-menu {}) + (map (fn [{:keys [label anchor]}] + (sidebar/sidebar-menu-item {:on-click (navigate-to-section! anchor)} label)) + items)))) + component-nav)) + (sidebar/sidebar-separator) (sidebar/sidebar-group {:label "Targets"} (into (sidebar/sidebar-menu {}) (map (fn [{:keys [label port active]}] diff --git a/scripts/check-dev.bb b/scripts/check-dev.bb new file mode 100755 index 0000000..b402c61 --- /dev/null +++ b/scripts/check-dev.bb @@ -0,0 +1,117 @@ +#!/usr/bin/env bb +;; Check all ui-dev tmux panes for compile errors and verify dev servers. +;; Exit 0 = all good, Exit 1 = errors found. + +(require '[babashka.process :as p] + '[clojure.string :as str] + '[babashka.http-client :as http]) + +;; ── Helpers ───────────────────────────────────────────────────────── + +(def red "\033[0;31m") +(def green "\033[0;32m") +(def yellow "\033[0;33m") +(def bold "\033[1m") +(def nc "\033[0m") + +(defn sh [& args] + (let [r (apply p/sh args)] + (when (zero? (:exit r)) + (str/trim (:out r))))) + +(defn println-ok [& parts] (println (str green "✓ " nc (str/join parts)))) +(defn println-err [& parts] (println (str red bold "✗ " nc (str/join parts)))) +(defn println-warn [& parts] (println (str yellow "⚠ " nc (str/join parts)))) + +(def errors (atom 0)) + +(defn fail! [] + (swap! errors inc)) + +;; ── 1. Tmux pane compile errors ──────────────────────────────────── + +(def error-patterns + #"(?i)Build failure|CompilerException|WARNING:|SyntaxError|^ERROR|Exception|error:|failed") + +(defn tmux-session-exists? [] + (some? (sh "tmux" "has-session" "-t" "ui-dev"))) + +(defn pane-indices [] + (some-> (sh "tmux" "list-panes" "-t" "ui-dev:1" "-F" "#{pane_index}") + (str/split-lines))) + +(defn capture-pane [idx] + (some-> (sh "tmux" "capture-pane" "-t" (str "ui-dev:1." idx) "-p" "-S" "-50") + (str/split-lines) + (->> (remove str/blank?) + (take-last 30) + vec))) + +(defn check-tmux-panes [] + (if-not (tmux-session-exists?) + (println-warn "No ui-dev tmux session found — skipping pane checks") + (doseq [idx (pane-indices)] + (let [lines (capture-pane idx) + matches (seq (filter #(re-find error-patterns %) lines))] + (if matches + (do (println-err "Pane " idx " — errors found:") + (doseq [m matches] (println (str " " m))) + (fail!)) + (println-ok "Pane " idx " — " (last lines))))))) + +;; ── 2. Hiccup server ─────────────────────────────────────────────── + +(defn check-hiccup [] + (try + (let [body (:body (http/get "http://localhost:4003" {:throw false :timeout 3000}))] + (if (str/includes? (or body "") "sidebar-layout") + (println-ok "Hiccup — serving (sidebar-layout found)") + (do (println-err "Hiccup — responds but page content missing") + (fail!)))) + (catch Exception _ + (println-warn "Hiccup — not responding on :4003")))) + +;; ── 3. Squint compiled output ─────────────────────────────────────── + +(defn check-squint [] + (let [dir (clojure.java.io/file "dev/squint/.compiled")] + (if-not (.isDirectory dir) + (println-warn "Squint — dev/squint/.compiled not found") + (let [mjs-files (->> (file-seq dir) + (filter #(str/ends-with? (.getName %) ".mjs")) + (filter #(.isFile %))) + broken (filter (fn [f] + (<= (count (str/split-lines (slurp f))) 1)) + mjs-files)] + (if (seq broken) + (doseq [f broken] + (println-err "Squint — empty/truncated: " (.getPath f)) + (fail!)) + (println-ok "Squint — all " (count mjs-files) " .mjs files have content")))))) + +;; ── 4. Replicant compiled JS ──────────────────────────────────────── + +(defn check-replicant [] + (let [dir (clojure.java.io/file "dev/replicant/public/js/cljs-runtime")] + (if-not (.isDirectory dir) + (println-warn "Replicant — cljs-runtime dir not found") + (let [ui-files (->> (.listFiles dir) + (filter #(str/starts-with? (.getName %) "ui.")) + (filter #(str/ends-with? (.getName %) ".js")))] + (if (seq ui-files) + (println-ok "Replicant — " (count ui-files) " ui.*.js files compiled") + (do (println-err "Replicant — no ui.*.js files compiled") + (fail!))))))) + +;; ── Run ───────────────────────────────────────────────────────────── + +(check-tmux-panes) +(check-hiccup) +(check-squint) +(check-replicant) + +(println) +(if (pos? @errors) + (do (println (str red bold "✗ " @errors " error(s) found — do not commit" nc)) + (System/exit 1)) + (println (str green bold "✓ All checks passed" nc)))