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.
This commit is contained in:
39
AGENTS.md
39
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/<module>.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/<module>.cljc
|
||||
cd dev/squint && npx squint compile src/dev/squint.cljs
|
||||
touch src/ui/<module>.cljc
|
||||
```
|
||||
|
||||
## Theme System
|
||||
|
||||
4
bb.edn
4
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]
|
||||
|
||||
@@ -72,9 +72,10 @@
|
||||
;; ── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
(defn section [title & children]
|
||||
[:section {:style "margin-bottom: 2.5rem;"}
|
||||
(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)])
|
||||
(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)]
|
||||
|
||||
@@ -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"}}
|
||||
(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)])
|
||||
(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)]
|
||||
|
||||
@@ -42,10 +42,11 @@
|
||||
(.removeAttribute layout "data-sidebar-open")))
|
||||
|
||||
(defn section [title & children]
|
||||
[:section {:style {"margin-bottom" "2.5rem"}}
|
||||
(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)])
|
||||
(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]}]
|
||||
|
||||
117
scripts/check-dev.bb
Executable file
117
scripts/check-dev.bb
Executable file
@@ -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)))
|
||||
Reference in New Issue
Block a user